Skip to content

OAuth API Reference ​

Complete reference for Flowsta OAuth 2.0 endpoints.

Base URL ​

https://auth-api.flowsta.com

All endpoints use this base URL.

Authentication ​

OAuth endpoints use different authentication methods:

EndpointAuthentication Method
/oauth/authorizeNone (public)
/oauth/tokenPKCE (client_id + code_verifier), or client_secret for confidential clients
/oauth/userinfoBearer token (Authorization header)
/oauth/revokeNone (client_id + client_secret optional; checked only when both are sent)

PKCE is Mandatory

All OAuth clients must use PKCE. Requests without code_challenge will be rejected. The @flowsta/login-button and @flowsta/auth SDKs handle this automatically.

No Client Secret Required

Flowsta uses OAuth 2.0 with PKCE, which means you don't need a client secret. PKCE provides security for browser and mobile apps without exposing secrets in frontend code.


Authorization Endpoint ​

GET /oauth/authorize ​

Initiates the OAuth flow by redirecting the user to the Flowsta login page.

Parameters ​

ParameterTypeRequiredDescription
response_typestring✅Must be code
client_idstring✅Your application's client ID
redirect_uristring✅One of your configured redirect URIs
scopestring❌Space-separated scopes (see Available Scopes); defaults to openid
statestring⚠️CSRF protection token (highly recommended); at least 8 characters when present
code_challengestring✅PKCE code challenge (SHA-256 of code_verifier, base64url-encoded)
code_challenge_methodstring✅Must be S256 (only supported method)

Example Request ​

GET https://auth-api.flowsta.com/oauth/authorize?
  response_type=code&
  client_id=abc123...&
  redirect_uri=https://yourapp.com/auth/callback&
  scope=openid%20display_name&
  state=random_state_string&
  code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
  code_challenge_method=S256

Response ​

Redirect to login page:

If the user is not signed in, they're sent to login.flowsta.com, where they approve the sign-in with their Flowsta Vault (the login page has no password field - the Vault signs a challenge, see Vault Sign-In). The API appends your app's name (and logo URL, if one is registered) for the login page to show:

https://login.flowsta.com/login?
  client_id=abc123...&
  redirect_uri=https://yourapp.com/auth/callback&
  scope=openid%20display_name&
  state=random_state_string&
  code_challenge=E9Melhoa...&
  code_challenge_method=S256&
  app_name=My+App

Redirect to callback:

After successful authentication and consent:

https://yourapp.com/auth/callback?
  code=def456...&
  state=random_state_string

Error Responses ​

Validation failures are answered by the API as JSON - the browser is not sent to your redirect_uri:

jsonc
// HTTP 400 Bad Request
{
  "error": "invalid_request",
  "error_description": "Missing required parameters: client_id, redirect_uri, response_type"
}

Only access_denied reaches your redirect URI, after the user chooses Don't allow on the consent screen:

https://yourapp.com/auth/callback?
  error=access_denied&
  error_description=User+denied+authorization&
  state=random_state_string

Error Codes:

StatusErrorDescription
400invalid_requestMissing or invalid parameters - including a redirect_uri that is not registered, a missing code_challenge, or a state shorter than 8 characters
401invalid_clientClient ID not found or app disabled
400unsupported_response_typeresponse_type is not code
400invalid_scopeA requested scope is unknown or not allowed for this app
redirectaccess_deniedUser chose Don't allow on the consent screen
500server_errorInternal server error

Token Endpoint ​

POST /oauth/token ​

Exchange authorization code for access token and refresh token.

Request Headers ​

http
Content-Type: application/json

Request Body ​

Grant Type: authorization_code

FieldTypeRequiredDescription
grant_typestring✅Must be authorization_code
codestring✅Authorization code from callback
redirect_uristring✅Must match the original redirect URI
client_idstring✅Your application's client ID
code_verifierstring✅PKCE code verifier

PKCE Security

PKCE replaces client secrets for all clients. The code_verifier proves you initiated the authorization request without needing to store secrets.

Grant Type: refresh_token

FieldTypeRequiredDescription
grant_typestring✅Must be refresh_token
refresh_tokenstring✅The refresh token
client_idstring✅Your application's client ID

Example Request (Authorization Code) ​

http
POST /oauth/token HTTP/1.1
Host: auth-api.flowsta.com
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "code": "def456...",
  "redirect_uri": "https://yourapp.com/auth/callback",
  "client_id": "flowsta_app_abc123...",
  "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}

Example Request (Refresh Token) ​

http
POST /oauth/token HTTP/1.1
Host: auth-api.flowsta.com
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "refresh_token": "ghi012...",
  "client_id": "flowsta_app_abc123..."
}

Success Response (Authorization Code Grant) ​

Status: 200 OK

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "ghi012...",
  "scope": "openid display_name"
}
FieldTypeDescription
access_tokenstringAccess token (valid for 24 hours)
token_typestringAlways Bearer
expires_innumberSeconds until token expires (86400 = 24 hours)
refresh_tokenstringRefresh token (valid for 30 days)
scopestringGranted scopes - what you requested, plus openid if you omitted it

Success Response (Refresh Token Grant) ​

Status: 200 OK

json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "scope": "openid display_name"
}

No Refresh Token Rotation

The refresh grant returns a new access token only - the response contains no refresh_token. Keep using your original refresh token; it expires 30 days after it was issued. When it expires, send the user through the authorization flow again.

Error Response ​

Status: 400 Bad Request

json
{
  "error": "invalid_grant",
  "error_description": "Authorization code is invalid, expired, or already used"
}

Error Codes:

StatusErrorDescription
400invalid_requestMissing required parameters (grant_type, client_id, code + redirect_uri, refresh_token, or a code_verifier / client_secret)
401invalid_clientClient ID not found, app disabled, or wrong client_secret
400invalid_grantAuthorization code invalid, expired, or already used; code issued to another client; redirect_uri mismatch; PKCE verifier mismatch; refresh token invalid, expired, or revoked; or a legacy account that must be upgraded in Flowsta Vault before it can sign in again
400unsupported_grant_typeOnly authorization_code and refresh_token are supported
429rate_limitedMore than 30 token requests in a minute from this IP + client ID (see Rate Limiting)
429mau_limit_reachedThe app has reached its free-tier limit for monthly active users

Access Tokens Are Opaque ​

Treat access tokens as opaque strings. They are signed with a server-only secret (HS256), carry no iss claim, and their internal claims are not part of the public contract - your app cannot verify them locally, and there is no public key to verify against.

To validate a token or get the identity behind it, call /oauth/userinfo. A 401 means the token is invalid, expired, or revoked.


User Info Endpoint ​

GET /oauth/userinfo ​

Retrieve user profile information using an access token.

Request Headers ​

http
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Example Request ​

http
GET /oauth/userinfo HTTP/1.1
Host: auth-api.flowsta.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Success Response ​

Status: 200 OK

With openid display_name scopes only:

json
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "name": "John Doe"
}

With all profile scopes:

json
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "name": "John Doe",
  "preferred_username": "johndoe",
  "did": "did:flowsta:uhCAk7JpEWfkiV_RdAFfCnRZcJ9PwJR4yTLN-E3EcVU7KYCnRRZc",
  "agent_pub_key": "uhCAk7JpEWfkiV_RdAFfCnRZcJ9PwJR4yTLN-E3EcVU7KYCnRRZc",
  "profile_picture": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0...",
  "has_custom_picture": true
}

Email Requires the User to Share It

Flowsta's server keeps only a hash of the user's email. The email scope asks the user to share their address at the consent screen; only then do email and email_verified appear in the response (verified addresses only). Revoking your app deletes the shared copy.

If the user didn't share one and your app needs it, ask them directly.

FieldTypeScopeDescription
substring (UUID)openidStable unique user identifier (always included)
namestringdisplay_nameUser's display name
preferred_usernamestring or nullusernameUsername (null if the user has not set one)
didstringdid or holochainW3C Decentralized Identifier
agent_pub_keystringpublic_key or holochainHolochain agent public key
profile_picturestringprofile_pictureProfile picture (base64 data URI or URL)
has_custom_picturebooleanprofile_pictureWhether user uploaded a custom picture
emailstringemailEmail address - present only if the user shared it at consent (verified only; deleted on revoke)
email_verifiedbooleanemailEmail verification status - present alongside email (always true when shared)

Error Response ​

Status: 401 Unauthorized

json
{
  "error": "invalid_token",
  "error_description": "Token has expired"
}

Error Codes:

StatusErrorerror_description
401invalid_tokenMissing or invalid Authorization header, Token has expired, Invalid token, Wrong token type (not an OAuth access token), or Client is no longer active (the app was disabled)
404not_foundUser not found

Scopes never produce an error here: the response simply contains only the fields your token's scopes cover.


Token Revocation Endpoint ​

POST /oauth/revoke ​

Revoke a refresh token to logout the user.

Request Headers ​

http
Content-Type: application/json

Request Body ​

FieldTypeRequiredDescription
tokenstring✅The refresh token to revoke (only refresh tokens can be revoked; access tokens expire on their own)
token_type_hintstring❌Accepted and recorded, not enforced - any value is treated as refresh_token
client_idstring❌Your application's client ID; checked only when client_secret is also sent

Example Request ​

http
POST /oauth/revoke HTTP/1.1
Host: auth-api.flowsta.com
Content-Type: application/json

{
  "token": "ghi012...",
  "token_type_hint": "refresh_token",
  "client_id": "flowsta_app_abc123..."
}

Success Response ​

Status: 200 OK

json
{
  "success": true
}

Error Response ​

Status: 400 Bad Request

json
{
  "error": "invalid_request",
  "error_description": "Missing token parameter"
}

Vault Sign-In (Device-Hosted Identities) ​

Flowsta identities sign in without a password field. When a user signs in on login.flowsta.com, their Flowsta Vault proves possession of their keys by signing a one-time challenge - no one in between. Two endpoints implement this. Your OAuth app does not call them - they're used by Flowsta's login page and clients, and are documented here so you understand the full flow. (Attempting a password login against a device-hosted account returns 403 with error: "device_hosted_account".)

POST /auth/vault/challenge ​

Request a one-time sign-in challenge.

Request Body ​

FieldTypeRequiredDescription
client_idstring❌Requesting client identifier (defaults to flowsta); 1-64 characters from A-Z a-z 0-9 . _ -, else 400 invalid_client_id

Success Response ​

Status: 200 OK

json
{
  "challenge": "flowsta-auth-challenge:v1:<nonce>:<client_id>",
  "expires_in": 300
}

The challenge is single-use, expires after expires_in seconds, and carries a fixed domain-separation prefix so it can never be confused with any other signature the Vault produces.

POST /auth/vault/token ​

Exchange a Vault-signed challenge for a session.

Request Body ​

FieldTypeRequiredDescription
challengestring✅The exact challenge string from /auth/vault/challenge
agent_pub_keystring✅The identity's Holochain agent public key
signaturestring✅Ed25519 signature over the challenge, produced by the Vault

Success Response ​

Status: 200 OK - also sets the flowsta_session SSO cookie (httpOnly, SameSite=Lax, domain .flowsta.com, 7 days), which is what lets /oauth/authorize skip the login prompt for already-signed-in users.

json
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": null,
    "agentPubKey": "uhCAk...",
    "did": "did:flowsta:uhCAk...",
    "emailVerified": true,
    "displayName": "John Doe",
    "username": "johndoe",
    "profilePicture": "data:image/svg+xml;base64,...",
    "hasCustomPicture": true,
    "hostingModel": "device-hosted",
    "loginMethod": "vault"
  }
}

Note email: null - the server never holds a device-hosted identity's plaintext email; the device knows its own email.

Error Responses ​

StatusErrorDescription
400invalid_inputBody failed validation (challenge, agent_pub_key, and signature are required strings)
400invalid_challengeChallenge doesn't carry the required prefix
401challenge_expiredChallenge unknown, already used, or expired - request a new one
400invalid_agent_pub_key / invalid_signatureMalformed key or signature
401signature_verification_failedSignature doesn't verify against the agent key
401unknown_agent_keyNo identity registered for this device key
403account_blockedIdentity blocked - contact support
403not_device_hostedA legacy account that isn't device-hosted yet - signing in with the Vault app completes the upgrade

PKCE (Proof Key for Code Exchange) ​

What is PKCE? ​

PKCE enhances OAuth security by preventing authorization code interception attacks. It's especially important for public clients (SPAs, mobile apps).

How PKCE Works ​

  1. Generate code verifier (random 43-128 character string)
  2. Generate code challenge (SHA256 hash of verifier, base64url encoded)
  3. Send code challenge with authorization request
  4. Send code verifier with token exchange request
  5. Server verifies that challenge matches verifier

Implementation ​

Browser, in a <script type="module"> (top-level await):

javascript
function randomString(length) {
  const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
  const bytes = new Uint8Array(length);
  crypto.getRandomValues(bytes);
  return Array.from(bytes, (b) => charset[b % charset.length]).join('');
}

function base64url(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

// 1. Generate code verifier (43-128 characters)
const codeVerifier = randomString(128);

// 2. Generate code challenge (SHA-256 of the verifier, base64url-encoded)
const codeChallenge = base64url(
  await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier))
);

// Store the verifier for the callback page
sessionStorage.setItem('code_verifier', codeVerifier);

// Use the challenge in the authorization request
const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'your_client_id',
  redirect_uri: 'https://yourapp.com/auth/callback',
  scope: 'openid display_name profile_picture',
  state: randomString(32),
  code_challenge: codeChallenge,
  code_challenge_method: 'S256',
});
const authUrl = `https://auth-api.flowsta.com/oauth/authorize?${params}`;

Button Widget Handles PKCE

The @flowsta/login-button package automatically handles PKCE for you. You don't need to implement it manually.


Rate Limiting ​

One endpoint is rate-limited: POST /oauth/token, at 30 requests per minute, counted per client IP address + client_id. /oauth/authorize, /oauth/userinfo, and /oauth/revoke have no request limit.

Every /oauth/token response carries the draft-6 standard headers:

http
RateLimit-Limit: 30
RateLimit-Remaining: 29
RateLimit-Reset: 60

RateLimit-Reset is the number of seconds until the window resets, not a timestamp.

429 Too Many Requests Response:

json
{
  "error": "rate_limited",
  "message": "Too many token requests. Please slow down."
}

Note the body carries message, not error_description.


Token Lifetimes ​

Token TypeLifetimeRenewable?
Authorization Code10 minutes❌ No
Access Token24 hours❌ No (use refresh token)
Refresh Token30 days❌ No rotation - a refresh returns a new access token only

The refresh token's 30-day expiration is fixed from the time it was issued. When it expires, send the user through the authorization flow again - if their Flowsta SSO session is still active (and, on a computer, their Vault holds the same identity), this happens without a login prompt.


Scopes ​

Available Scopes ​

Flowsta uses granular scopes so users only consent to the specific data your app needs:

ScopeData IncludedDescription
openidsubUser ID (UUID) - auto-included
display_namenameUser's display name
usernamepreferred_usernameUser's @username
emailemail, email_verifiedEmail address - only if the user shared it at consent (verified only; deleted on revoke)
profile_pictureprofile_picture, has_custom_pictureProfile picture
diddidW3C Decentralized Identifier
public_keyagent_pub_keyHolochain agent public key
holochainagent_pub_key, didHolochain identity access (agent key and DID in /userinfo)
sign-Authorizes calls to Sign It signing endpoints on the user's behalf
verify-Authorizes calls to Sign It verification endpoints

Request Only What You Need

Instead of requesting all scopes, choose only the ones your app actually needs. This builds user trust and simplifies the consent screen.

Requesting Multiple Scopes ​

Separate scopes with spaces:

scope=openid display_name profile_picture

URL encoded:

scope=openid%20display_name%20profile_picture

No Partial Grants ​

Consent is all-or-nothing. The user either allows every scope you requested or denies the request (access_denied). A scope your app is not allowed to request is refused up front with invalid_scope, and the token's scope is exactly what you requested - plus openid if you omitted it. There is no downgrading.


Security Best Practices ​

1. PKCE is Required ​

PKCE is mandatory for all clients. Requests without code_challenge will be rejected.

javascript
// ✅ Required: With PKCE
const authUrl = buildAuthUrl({
  code_challenge: codeChallenge,
  code_challenge_method: 'S256'
});

// ❌ Will fail: Without PKCE
const authUrl = buildAuthUrl({});

2. Always Use State Parameter ​

javascript
// ✅ Good: With state for CSRF protection (at least 8 characters)
const state = Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
sessionStorage.setItem('oauth_state', state);

// ❌ Bad: Without state
const authUrl = buildAuthUrl({ /* no state */ });

3. Verify State in Callback ​

javascript
// ✅ Good: Verify state
const callbackState = new URLSearchParams(window.location.search).get('state');
const storedState = sessionStorage.getItem('oauth_state');

if (callbackState !== storedState) {
  throw new Error('Possible CSRF attack');
}

4. Use HTTPS ​

Use HTTPS for every production redirect URI. When you register a URI, Flowsta accepts both http: and https: (so http://localhost:3000/callback works for development) and checks for an exact string match at authorization time - it does not reject a plain-http production URI for you.

javascript
// ✅ Good
redirect_uri: 'https://yourapp.com/callback'

// ❌ Bad (except localhost)
redirect_uri: 'http://yourapp.com/callback'

Testing ​

Sign in with your own Flowsta identity (created in Flowsta Vault), using your app's real Client ID and a http://localhost:... redirect URI registered on dev.flowsta.com.


Need Help? ​

Documentation licensed under CC BY-SA 4.0.