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)
/oauth/userinfoBearer token (Authorization header)
/oauth/revokeClient ID in request body (client_id)

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_typestringMust be code
client_idstringYour application's client ID
redirect_uristringOne of your configured redirect URIs
scopestringSpace-separated scopes (see Available Scopes)
statestring⚠️CSRF protection token (highly recommended)
code_challengestringPKCE code challenge (SHA-256 of code_verifier, base64url-encoded)
code_challenge_methodstringMust 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 (device-hosted accounts have no password - the Vault signs a challenge, see Vault Sign-In):

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

Redirect to callback:

After successful authentication and consent:

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

Error Responses

If an error occurs, the user is redirected to redirect_uri with error parameters:

https://yourapp.com/auth/callback?
  error=invalid_request&
  error_description=Missing+required+parameter+client_id&
  state=random_state_string

Error Codes:

ErrorDescription
invalid_requestMissing or invalid required parameters
unauthorized_clientClient ID not found or app disabled
access_deniedUser denied authorization
unsupported_response_typeresponse_type is not code
invalid_scopeOne or more requested scopes are invalid
server_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_typestringMust be authorization_code
codestringAuthorization code from callback
redirect_uristringMust match the original redirect URI
client_idstringYour application's client ID
code_verifierstringPKCE 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_typestringMust be refresh_token
refresh_tokenstringThe refresh token
client_idstringYour 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 (may differ from requested)

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 has expired or is invalid"
}

Error Codes:

ErrorDescription
invalid_requestMissing required parameters
invalid_clientInvalid client credentials
invalid_grantInvalid or expired authorization code
unauthorized_clientClient not authorized for this grant type
unsupported_grant_typeGrant type not supported

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_usernamestringusernameUsername (if set by user)
didstringdidW3C Decentralized Identifier
agent_pub_keystringpublic_keyHolochain 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": "Access token is invalid or expired"
}

Error Codes:

ErrorDescription
invalid_tokenToken is invalid, expired, or revoked
insufficient_scopeToken doesn't have required scopes

Token Revocation Endpoint

POST /oauth/revoke

Revoke a refresh token to logout the user.

Request Headers

http
Content-Type: application/json

Request Body

FieldTypeRequiredDescription
tokenstringThe refresh token to revoke
token_type_hintstringMust be refresh_token
client_idstringYour application's client ID

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": "Token parameter is required"
}

Vault Sign-In (Device-Hosted Accounts)

Flowsta accounts have no passwords. 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_idstringRequesting client identifier (defaults to flowsta)

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
challengestringThe exact challenge string from /auth/vault/challenge
agent_pub_keystringThe account's Holochain agent public key
signaturestringEd25519 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 account's plaintext email; the device knows its own email.

Error Responses

StatusErrorDescription
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 account for this device key
403account_blockedAccount blocked - contact support
403not_device_hostedThis account 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

javascript
import crypto from 'crypto';

// 1. Generate code verifier
function generateCodeVerifier() {
  return crypto.randomBytes(32).toString('base64url');
}

// 2. Generate code challenge
function generateCodeChallenge(verifier) {
  return crypto
    .createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// Store verifier in session for later use
sessionStorage.setItem('code_verifier', codeVerifier);

// Use challenge in authorization request
const authUrl = `https://auth-api.flowsta.com/oauth/authorize?
  response_type=code&
  client_id=${clientId}&
  redirect_uri=${redirectUri}&
  scope=openid%20display_name%20profile_picture&
  code_challenge=${codeChallenge}&
  code_challenge_method=S256`;

Button Widget Handles PKCE

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


Rate Limiting

OAuth endpoints are rate-limited to prevent abuse:

EndpointRate Limit
/oauth/authorize100 requests per minute per IP
/oauth/token50 requests per minute per client
/oauth/userinfo500 requests per minute per token
/oauth/revoke50 requests per minute per client

Rate Limit Headers:

http
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1699564800

429 Too Many Requests Response:

json
{
  "error": "rate_limit_exceeded",
  "error_description": "Too many requests. Please try again later.",
  "retry_after": 60
}

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, 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_keyHolochain identity access (includes agent key 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

Scope Downgrading

If a user hasn't granted permission for a requested scope, the token will be issued with reduced scopes.

Request: scope=openid display_name email
User hasn't granted email permission
Token issued with: scope=openid display_name

Always check the scope field in the token response to see what was actually granted.


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
const state = crypto.randomBytes(16).toString('hex');
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

All redirect URIs must use HTTPS in production:

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

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

Testing

Create test accounts at dev.flowsta.com and use your app's real Client ID with a http://localhost:... redirect URI during development.


Need Help?

Documentation licensed under CC BY-SA 4.0.