OAuth API Reference
Complete reference for Flowsta OAuth 2.0 endpoints.
Base URL
https://auth-api.flowsta.comAll endpoints use this base URL.
Authentication
OAuth endpoints use different authentication methods:
| Endpoint | Authentication Method |
|---|---|
/oauth/authorize | None (public) |
/oauth/token | PKCE (client_id + code_verifier) |
/oauth/userinfo | Bearer token (Authorization header) |
/oauth/revoke | Client 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
| Parameter | Type | Required | Description |
|---|---|---|---|
response_type | string | ✅ | Must be code |
client_id | string | ✅ | Your application's client ID |
redirect_uri | string | ✅ | One of your configured redirect URIs |
scope | string | ✅ | Space-separated scopes (see Available Scopes) |
state | string | ⚠️ | CSRF protection token (highly recommended) |
code_challenge | string | ✅ | PKCE code challenge (SHA-256 of code_verifier, base64url-encoded) |
code_challenge_method | string | ✅ | 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=S256Response
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=S256Redirect to callback:
After successful authentication and consent:
https://yourapp.com/auth/callback?
code=def456...&
state=random_state_stringError 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_stringError Codes:
| Error | Description |
|---|---|
invalid_request | Missing or invalid required parameters |
unauthorized_client | Client ID not found or app disabled |
access_denied | User denied authorization |
unsupported_response_type | response_type is not code |
invalid_scope | One or more requested scopes are invalid |
server_error | Internal server error |
Token Endpoint
POST /oauth/token
Exchange authorization code for access token and refresh token.
Request Headers
Content-Type: application/jsonRequest Body
Grant Type: authorization_code
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | ✅ | Must be authorization_code |
code | string | ✅ | Authorization code from callback |
redirect_uri | string | ✅ | Must match the original redirect URI |
client_id | string | ✅ | Your application's client ID |
code_verifier | string | ✅ | 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
| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | ✅ | Must be refresh_token |
refresh_token | string | ✅ | The refresh token |
client_id | string | ✅ | Your application's client ID |
Example Request (Authorization Code)
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)
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
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 86400,
"refresh_token": "ghi012...",
"scope": "openid display_name"
}| Field | Type | Description |
|---|---|---|
access_token | string | Access token (valid for 24 hours) |
token_type | string | Always Bearer |
expires_in | number | Seconds until token expires (86400 = 24 hours) |
refresh_token | string | Refresh token (valid for 30 days) |
scope | string | Granted scopes (may differ from requested) |
Success Response (Refresh Token Grant)
Status: 200 OK
{
"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
{
"error": "invalid_grant",
"error_description": "Authorization code has expired or is invalid"
}Error Codes:
| Error | Description |
|---|---|
invalid_request | Missing required parameters |
invalid_client | Invalid client credentials |
invalid_grant | Invalid or expired authorization code |
unauthorized_client | Client not authorized for this grant type |
unsupported_grant_type | Grant 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
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Example Request
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:
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe"
}With all profile scopes:
{
"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.
| Field | Type | Scope | Description |
|---|---|---|---|
sub | string (UUID) | openid | Stable unique user identifier (always included) |
name | string | display_name | User's display name |
preferred_username | string | username | Username (if set by user) |
did | string | did | W3C Decentralized Identifier |
agent_pub_key | string | public_key | Holochain agent public key |
profile_picture | string | profile_picture | Profile picture (base64 data URI or URL) |
has_custom_picture | boolean | profile_picture | Whether user uploaded a custom picture |
email | string | email | Email address - present only if the user shared it at consent (verified only; deleted on revoke) |
email_verified | boolean | email | Email verification status - present alongside email (always true when shared) |
Error Response
Status: 401 Unauthorized
{
"error": "invalid_token",
"error_description": "Access token is invalid or expired"
}Error Codes:
| Error | Description |
|---|---|
invalid_token | Token is invalid, expired, or revoked |
insufficient_scope | Token doesn't have required scopes |
Token Revocation Endpoint
POST /oauth/revoke
Revoke a refresh token to logout the user.
Request Headers
Content-Type: application/jsonRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
token | string | ✅ | The refresh token to revoke |
token_type_hint | string | ❌ | Must be refresh_token |
client_id | string | ✅ | Your application's client ID |
Example Request
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
{
"success": true
}Error Response
Status: 400 Bad Request
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string | ❌ | Requesting client identifier (defaults to flowsta) |
Success Response
Status: 200 OK
{
"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
| Field | Type | Required | Description |
|---|---|---|---|
challenge | string | ✅ | The exact challenge string from /auth/vault/challenge |
agent_pub_key | string | ✅ | The account's Holochain agent public key |
signature | string | ✅ | 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.
{
"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
| Status | Error | Description |
|---|---|---|
| 400 | invalid_challenge | Challenge doesn't carry the required prefix |
| 401 | challenge_expired | Challenge unknown, already used, or expired - request a new one |
| 400 | invalid_agent_pub_key / invalid_signature | Malformed key or signature |
| 401 | signature_verification_failed | Signature doesn't verify against the agent key |
| 401 | unknown_agent_key | No account for this device key |
| 403 | account_blocked | Account blocked - contact support |
| 403 | not_device_hosted | This 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
- Generate code verifier (random 43-128 character string)
- Generate code challenge (SHA256 hash of verifier, base64url encoded)
- Send code challenge with authorization request
- Send code verifier with token exchange request
- Server verifies that challenge matches verifier
Implementation
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:
| Endpoint | Rate Limit |
|---|---|
/oauth/authorize | 100 requests per minute per IP |
/oauth/token | 50 requests per minute per client |
/oauth/userinfo | 500 requests per minute per token |
/oauth/revoke | 50 requests per minute per client |
Rate Limit Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1699564800429 Too Many Requests Response:
{
"error": "rate_limit_exceeded",
"error_description": "Too many requests. Please try again later.",
"retry_after": 60
}Token Lifetimes
| Token Type | Lifetime | Renewable? |
|---|---|---|
| Authorization Code | 10 minutes | ❌ No |
| Access Token | 24 hours | ❌ No (use refresh token) |
| Refresh Token | 30 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:
| Scope | Data Included | Description |
|---|---|---|
openid | sub | User ID (UUID) - auto-included |
display_name | name | User's display name |
username | preferred_username | User's @username |
email | email, email_verified | Email address - only if the user shared it at consent (verified only; deleted on revoke) |
profile_picture | profile_picture, has_custom_picture | Profile picture |
did | did | W3C Decentralized Identifier |
public_key | agent_pub_key | Holochain agent public key |
holochain | agent_pub_key | Holochain 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_pictureURL encoded:
scope=openid%20display_name%20profile_pictureScope 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.
// ✅ 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
// ✅ 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
// ✅ 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:
// ✅ 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?
- 💬 Discord: Join our community
- 🆘 Support: Find out about Flowsta support options
- 🐙 GitHub: github.com/WeAreFlowsta