Authentication API
The Authentication API provides comprehensive endpoints for user authentication, including OAuth flows, password-based authentication, device authorization, MFA (Multi-Factor Authentication), and session management. This API supports both administrative authentication and end-user SSO flows.
Overview
The SSO platform implements multiple authentication strategies:
- OAuth 2.0 Flows: Support for GitHub, Google, and Microsoft OAuth providers with BYOO (Bring Your Own OAuth) capability
- Password Authentication: Email/password registration and login with email verification
- Device Authorization Flow: RFC 8628 implementation for CLIs and headless devices
- Multi-Factor Authentication (MFA): TOTP-based second factor with backup codes
- Admin Authentication: Dedicated OAuth flow for platform owners and organization administrators
- Passkey Authentication (WebAuthn): FIDO2/WebAuthn passwordless authentication
- Magic Link Authentication: Passwordless authentication via email link
- Session Management: JWT-based authentication with refresh token rotation
Endpoints Summary
| Method | Path | Description |
|---|---|---|
GET |
/.well-known/jwks.json |
Get JWT public keys (JWKS) |
GET |
/auth/:provider |
Initiate end-user OAuth login |
GET |
/auth/:provider/callback |
Handle OAuth callback |
GET |
/auth/admin/:provider |
Initiate admin OAuth login |
GET |
/auth/admin/:provider/callback |
Handle admin OAuth callback |
POST |
/api/auth/register |
Register new user with email/password |
GET |
/auth/verify-email |
Verify email address |
POST |
/api/auth/login |
Login with email/password |
POST |
/api/auth/mfa/verify |
Verify MFA code during login |
POST |
/api/auth/forgot-password |
Request password reset |
POST |
/api/auth/reset-password |
Reset password with token |
POST |
/api/auth/magic-link/request |
Request passwordless magic link |
GET |
/api/auth/magic-link/verify |
Verify magic link and authenticate |
POST |
/auth/passkeys/register/start |
Start passkey registration ceremony |
POST |
/auth/passkeys/register/finish |
Complete passkey registration |
POST |
/auth/passkeys/authenticate/start |
Start passkey authentication |
POST |
/auth/passkeys/authenticate/finish |
Complete passkey authentication |
POST |
/auth/device/code |
Request device authorization codes |
POST |
/auth/device/verify |
Verify user code and get context |
POST |
/auth/token |
Exchange device code for access token |
POST |
/api/auth/logout |
Logout and revoke session |
POST |
/api/auth/refresh |
Refresh access token |
Public Endpoints
GET /.well-known/jwks.json
Retrieve the JSON Web Key Set (JWKS) containing the public RSA key(s) used to verify JWT signatures. This enables third-party backends to validate JWTs without accessing any shared secrets.
Permissions: Public (no authentication required)
Response (200 OK):
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "sso-key-2025-01-01",
"n": "base64url-encoded-modulus",
"e": "base64url-encoded-exponent"
}
]
}
Example Request:
curl https://sso.example.com/.well-known/jwks.json
OAuth 2.0 Authentication
GET /auth/:provider
Initiate OAuth 2.0 authentication flow for end-users. Supports GitHub, Google, and Microsoft providers. Organizations can use their own OAuth credentials (BYOO) or fall back to platform defaults.
Permissions: Public (no authentication required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
provider |
string | OAuth provider: github, google, or microsoft |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
org |
string | Yes | Organization slug |
service |
string | Yes | Service slug |
redirect_uri |
string | No | Callback URL (must be in service’s allowed URIs) |
user_code |
string | No | Device flow user code (for device authorization) |
saml_state |
string | No | SAML state ID (for SAML SSO flows) |
Example Request:
curl -X GET "https://sso.example.com/auth/github?org=acme-corp&service=main-app&redirect_uri=https://app.acme.com/callback"
Response: 302 Redirect to OAuth provider’s authorization page
Error Responses:
404 Not Found: Organization or service not found400 Bad Request: Invalid redirect_uri or missing required parameters
GET /auth/:provider/callback
Handle OAuth callback from provider. Exchanges authorization code for access token, creates or updates user account, and generates JWT session.
Permissions: Public (no authentication required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
provider |
string | OAuth provider: github, google, or microsoft |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | Authorization code from OAuth provider |
state |
string | No | CSRF protection state parameter |
Success Response: Redirects to service’s redirect_uri with tokens:
https://app.acme.com/callback?access_token={jwt}&refresh_token={token}
Or returns HTML success page if no redirect_uri configured.
Example Request:
# This is typically called by the OAuth provider after user authorization
curl -X GET "https://sso.example.com/auth/github/callback?code=abc123&state=csrf_token"
Error Response: HTML error page with user-friendly message
Admin OAuth Authentication
GET /auth/admin/:provider
Initiate OAuth flow for platform owners and organization administrators. Uses dedicated platform OAuth credentials.
Permissions: Public (no authentication required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
provider |
string | OAuth provider: github, google, or microsoft |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
org_slug |
string | No | Organization slug (for org admin context) |
user_code |
string | No | Device flow user code |
Example Request:
curl -X GET "https://sso.example.com/auth/admin/github?org_slug=acme-corp"
Response: 302 Redirect to OAuth provider’s authorization page
GET /auth/admin/:provider/callback
Handle admin OAuth callback. Creates admin JWT with appropriate permissions (platform owner or organization admin).
Permissions: Public (no authentication required)
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
provider |
string | OAuth provider: github, google, or microsoft |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | Authorization code from OAuth provider |
state |
string | No | CSRF protection state parameter |
Success Response: Redirects to platform admin redirect URI with tokens
Example Request:
# This is typically called by the OAuth provider after admin authorization
curl -X GET "https://sso.example.com/auth/admin/github/callback?code=abc123&state=csrf_token"
Password Authentication
POST /api/auth/register
Register a new user account with email and password. Sends verification email to confirm email address.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | Valid email address |
password |
string | Yes | Password (minimum 8 characters) |
org_slug |
string | No | Organization slug (for org-specific email service) |
Example Request:
curl -X POST https://sso.example.com/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"org_slug": "acme-corp"
}'
Success Response (200 OK):
{
"message": "Registration successful. Please check your email to verify your account."
}
Error Responses:
400 Bad Request: Invalid email format, weak password, or user already exists{ "error": "User with this email already exists" }
GET /auth/verify-email
Verify user’s email address using verification token sent via email.
Permissions: Public (no authentication required)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Email verification token (UUID) |
Example Request:
curl -X GET "https://sso.example.com/auth/verify-email?token=550e8400-e29b-41d4-a716-446655440000"
Success Response (200 OK): HTML page confirming email verification
Error Responses:
400 Bad Request: Invalid, expired, or already used token{ "error": "Verification token has expired" }
POST /api/auth/login
Authenticate user with email and password. Returns JWT access token and refresh token, or pre-auth token if MFA is enabled.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User’s email address |
password |
string | Yes | User’s password |
saml_state |
string | No | SAML state ID (for SAML flows) |
Example Request:
curl -X POST https://sso.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123"
}'
Success Response (200 OK) - Without MFA:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "550e8400-e29b-41d4-a716-446655440000",
"expires_in": 86400
}
Success Response (200 OK) - With MFA Enabled:
{
"access_token": "preauth_token_eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "",
"expires_in": 600,
"mfa_required": true
}
Error Responses:
401 Unauthorized: Invalid credentials or unverified email{ "error": "Invalid email or password" }{ "error": "Please verify your email address before logging in" }
POST /api/auth/forgot-password
Request password reset email. Always returns success to prevent email enumeration attacks.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User’s email address |
org_slug |
string | No | Organization slug (for org-specific email service) |
Example Request:
curl -X POST https://sso.example.com/api/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"org_slug": "acme-corp"
}'
Success Response (200 OK):
{
"message": "If an account with that email exists, a password reset link has been sent."
}
POST /api/auth/reset-password
Reset user’s password using token from password reset email. Revokes all existing sessions for security.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Password reset token (UUID) |
new_password |
string | Yes | New password (minimum 8 characters) |
Example Request:
curl -X POST https://sso.example.com/api/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "550e8400-e29b-41d4-a716-446655440000",
"new_password": "newsecurepassword456"
}'
Success Response (200 OK):
{
"message": "Password has been reset successfully. Please log in with your new password."
}
Error Responses:
400 Bad Request: Invalid, expired, or already used token; weak password{ "error": "Reset token has expired" }
Multi-Factor Authentication
POST /api/auth/mfa/verify
Verify MFA code (TOTP or backup code) and complete authentication. Exchanges pre-auth token for full access token.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
preauth_token |
string | Yes | Pre-authentication JWT from login |
code |
string | Yes | 6-digit TOTP code or backup code |
device_code_id |
string | No | Device code ID (for device flow completion) |
Example Request:
curl -X POST https://sso.example.com/api/auth/mfa/verify \
-H "Content-Type: application/json" \
-d '{
"preauth_token": "preauth_token_eyJhbGciOiJSUzI1NiIs...",
"code": "123456"
}'
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "550e8400-e29b-41d4-a716-446655440000",
"expires_in": 86400
}
Error Responses:
400 Bad Request: Invalid pre-auth token or incorrect MFA code{ "error": "Invalid MFA code" }429 Too Many Requests: Rate limit exceeded for MFA attempts{ "error": "Too many failed attempts. Please try again later." }
Device Authorization Flow (RFC 8628)
POST /auth/device/code
Request device authorization codes for CLI tools and headless devices. Implements RFC 8628 device authorization grant.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
client_id |
string | Yes | Service client ID |
org |
string | Yes | Organization slug |
service |
string | Yes | Service slug |
Example Request:
curl -X POST https://sso.example.com/auth/device/code \
-H "Content-Type: application/json" \
-d '{
"client_id": "service_abc123xyz",
"org": "acme-corp",
"service": "cli-tool"
}'
Success Response (200 OK):
{
"device_code": "550e8400-e29b-41d4-a716-446655440000",
"user_code": "ABCD-EFGH",
"verification_uri": "https://sso.example.com/device",
"expires_in": 900,
"interval": 5
}
Error Responses:
400 Bad Request: Invalid client_id or service not configured for device flow{ "error": "Device activation URI not configured for this service" }
Usage:
- CLI displays
user_codeandverification_urito user - User visits
verification_uriin browser and entersuser_code - CLI polls
/auth/tokenendpoint every 5 seconds usingdevice_code
POST /auth/device/verify
Verify user code and return authentication context. Called by web frontend when user enters device code.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
user_code |
string | Yes | User code displayed on device (e.g., “ABCD-EFGH”) |
Example Request:
curl -X POST https://sso.example.com/auth/device/verify \
-H "Content-Type: application/json" \
-d '{
"user_code": "ABCD-EFGH"
}'
Success Response (200 OK):
{
"org_slug": "acme-corp",
"service_slug": "cli-tool",
"available_providers": ["github", "google", "microsoft"]
}
Error Responses:
400 Bad Request: Invalid, expired, or already authorized user code{ "error": "Invalid user code" }{ "error": "Device already authorized" }
POST /auth/token
Exchange device code for access token. Implements polling endpoint for device authorization flow.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
grant_type |
string | Yes | Must be urn:ietf:params:oauth:grant-type:device_code |
client_id |
string | Yes | Service client ID |
device_code |
string | Yes | Device code from /auth/device/code |
Example Request:
curl -X POST https://sso.example.com/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"client_id": "service_abc123xyz",
"device_code": "550e8400-e29b-41d4-a716-446655440000"
}'
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400
}
Error Responses:
400 Bad Request: Invalid grant_type or device code{ "error": "Invalid grant type" }401 Unauthorized: Device not yet authorized (continue polling){ "error": "Not authorized" }400 Bad Request: Device code expired{ "error": "Device code expired" }
Polling Behavior:
- Poll every 5 seconds (specified in
intervalfrom/auth/device/code) - Continue polling until you receive an access token or error
Not authorizedmeans user hasn’t authorized yet- Stop polling on any other error
Magic Link Authentication
Passwordless authentication via email link provides a simplified login experience without requiring password management.
POST /api/auth/magic-link/request
Request a magic link to be sent to the user’s email address.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User’s email address |
org_slug |
string | No | Organization context for branded emails |
Example Request:
curl -X POST https://sso.example.com/api/auth/magic-link/request \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"org_slug": "acme-corp"
}'
Success Response (200 OK):
{
"message": "If the email exists, a magic link has been sent."
}
Features:
- Email Enumeration Protection: Always returns success message regardless of email existence
- Rate Limiting: Prevents abuse (configurable via
DISABLE_RATE_LIMITINGenv var) - Expiration: Tokens expire after 15 minutes
- One-Time Use: Tokens are deleted after successful verification
- Organization Context: Sends emails using organization-specific SMTP if configured
Error Responses:
400 Bad Request: Invalid email format{ "error": "Invalid email format" }429 Too Many Requests: Rate limit exceeded{ "error": "Too many magic link requests. Please try again later." }
GET /api/auth/magic-link/verify
Verify a magic link token and authenticate the user.
Permissions: Public (no authentication required)
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
token |
string | Yes | Magic link token from email |
redirect_uri |
string | No | Optional redirect URL after authentication |
Example Request:
curl -X GET "https://sso.example.com/api/auth/magic-link/verify?token=magic-token-uuid" \
-H "Content-Type: application/json"
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "550e8400-e29b-41d4-a716-446655440000",
"expires_in": 900
}
Risk Assessment Response (200 OK - High Risk):
{
"requires_mfa": true,
"preauth_token": "eyJhbGciOiJSUzI1NiIs...",
"message": "Additional verification required"
}
Features:
- Risk Engine Integration: Evaluates login risk based on IP, location, device trust
- Device Trust: Establishes 90-day trusted device cookie on success
- Session Creation: Creates session with refresh token
- Automatic Cleanup: Deletes magic link token after use
Error Responses:
400 Bad Request: Invalid or expired magic link{ "error": "Invalid or expired magic link" }400 Bad Request: User not found{ "error": "User not found. Please register first." }403 Forbidden: Blocked by risk engine{ "error": "Suspicious login detected. Please contact support." }
Set-Cookie Header (if device trust established):
Set-Cookie: device_token={signed-token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=7776000
Passkey Authentication (WebAuthn)
FIDO2/WebAuthn-based passwordless authentication using biometrics or hardware security keys.
POST /auth/passkeys/register/start
Start passkey registration ceremony. Requires an authenticated user session.
Permissions: Authenticated user (requires valid JWT)
Headers:
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer {jwt} |
Yes |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | No | Optional name for the passkey (e.g., “My YubiKey”) |
Example Request:
curl -X POST https://sso.example.com/auth/passkeys/register/start \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "My Security Key"
}'
Success Response (200 OK):
{
"challenge_id": "challenge-uuid-123",
"options": {
"challenge": "Y2hhbGxlbmdlLWJhc2U2NC1lbmNvZGVk",
"rp": {
"name": "AuthOS",
"id": "sso.example.com"
},
"user": {
"id": "user-id-456",
"name": "user@example.com",
"displayName": "user@example.com"
},
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
},
{
"type": "public-key",
"alg": -257
}
],
"timeout": 60000,
"excludeCredentials": [],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"requireResidentKey": false,
"userVerification": "preferred"
}
}
}
Usage: Pass options to navigator.credentials.create() in the browser.
Challenge Expiration: 5 minutes
POST /auth/passkeys/register/finish
Complete passkey registration ceremony by submitting the WebAuthn credential.
Permissions: Authenticated user (requires valid JWT)
Headers:
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer {jwt} |
Yes |
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id |
string | Yes | Challenge ID from registration start |
credential |
object | Yes | WebAuthn PublicKeyCredential from browser |
Example Request:
curl -X POST https://sso.example.com/auth/passkeys/register/finish \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"challenge_id": "challenge-uuid-123",
"credential": {
"id": "credential-id",
"rawId": "base64-raw-id",
"response": {
"clientDataJSON": "base64-client-data",
"attestationObject": "base64-attestation"
},
"type": "public-key"
}
}'
Success Response (200 OK):
{
"success": true,
"passkey_id": "passkey-id-789"
}
Features:
- Stores passkey with counter tracking for clone detection
- Associates passkey with user account
- Deletes challenge after successful registration
Error Responses:
400 Bad Request: Invalid or expired challenge401 Unauthorized: Challenge doesn’t belong to user500 Internal Server Error: WebAuthn verification failed
POST /auth/passkeys/authenticate/start
Start passkey authentication ceremony. Public endpoint for passwordless login.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | Yes | User’s email address |
Example Request:
curl -X POST https://sso.example.com/auth/passkeys/authenticate/start \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
Success Response (200 OK):
{
"challenge_id": "challenge-uuid-456",
"options": {
"challenge": "Y2hhbGxlbmdlLWJhc2U2NC1lbmNvZGVk",
"timeout": 60000,
"rpId": "sso.example.com",
"allowCredentials": [
{
"type": "public-key",
"id": "base64-credential-id-1"
},
{
"type": "public-key",
"id": "base64-credential-id-2"
}
],
"userVerification": "preferred"
}
}
Usage: Pass options to navigator.credentials.get() in the browser.
Error Responses:
404 Not Found: User not found{ "error": "User not found" }400 Bad Request: No passkeys registered{ "error": "No passkeys registered for this user" }
POST /auth/passkeys/authenticate/finish
Complete passkey authentication ceremony and issue JWT.
Permissions: Public (no authentication required)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
challenge_id |
string | Yes | Challenge ID from authentication start |
credential |
object | Yes | WebAuthn PublicKeyCredential from browser |
Example Request:
curl -X POST https://sso.example.com/auth/passkeys/authenticate/finish \
-H "Content-Type: application/json" \
-d '{
"challenge_id": "challenge-uuid-456",
"credential": {
"id": "credential-id",
"rawId": "base64-raw-id",
"response": {
"clientDataJSON": "base64-client-data",
"authenticatorData": "base64-authenticator-data",
"signature": "base64-signature",
"userHandle": "base64-user-handle"
},
"type": "public-key"
}
}'
Success Response (200 OK):
{
"token": "eyJhbGciOiJSUzI1NiIs...",
"user_id": "user-id-456",
"device_trust_token": "device-trust-token-xyz"
}
Features:
- Risk Engine Integration: Evaluates authentication risk
- Counter Validation: Updates and validates authenticator counter to detect clones
- Device Trust: Issues device trust token for low-risk logins
- Challenge Cleanup: Deletes challenge after successful authentication
Error Responses:
400 Bad Request: Invalid or expired challenge403 Forbidden: Blocked by risk engine (high risk score){ "error": "Suspicious login detected. Please contact support." }403 Forbidden: Risk engine requires MFA{ "error": "Additional verification required. Please use another login method." }
Set-Cookie Header (if device trust established):
Set-Cookie: device_token={signed-token}; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=7776000
Security Features:
- Phishing-resistant (origin-bound credentials)
- Clone detection via counter tracking
- Integrated risk assessment
- Device trust establishment
Session Management
POST /api/auth/logout
Logout and revoke current session. Invalidates the JWT and removes the refresh token.
Permissions: Authenticated user (requires valid JWT)
Headers:
| Header | Value | Required |
|---|---|---|
Authorization |
Bearer {jwt} |
Yes |
Example Request:
curl -X POST https://sso.example.com/api/auth/logout \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Success Response: 204 No Content
Error Responses:
401 Unauthorized: Missing or invalid JWT{ "error": "Missing or invalid Authorization header" }
POST /api/auth/refresh
Refresh access token using refresh token. Implements token rotation for enhanced security - both access token and refresh token are replaced.
Permissions: Public (no authentication required, uses refresh token)
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
refresh_token |
string | Yes | Valid refresh token from login or previous refresh |
Example Request:
curl -X POST https://sso.example.com/api/auth/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "550e8400-e29b-41d4-a716-446655440000"
}'
Success Response (200 OK):
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "660e8400-e29b-41d4-a716-446655440111",
"expires_in": 86400
}
Error Responses:
401 Unauthorized: Invalid or expired refresh token{ "error": "Invalid refresh token" }{ "error": "Refresh token expired" }
Important Notes:
- Old refresh token is immediately invalidated (token rotation)
- Always store the new refresh token returned in the response
- Refresh tokens expire after 30 days of inactivity
- Access tokens expire after 24 hours (configurable via
JWT_EXPIRE_HOURS)
JWT Structure
JWTs issued by the authentication API use RS256 signing and contain the following claims:
{
"sub": "user_id",
"email": "user@example.com",
"is_platform_owner": false,
"org": "organization_slug",
"service": "service_slug",
"plan": "plan_name",
"features": ["feature1", "feature2"],
"exp": 1672531199,
"iat": 1672444800,
"kid": "sso-key-2025-01-01"
}
JWT Types
1. Platform Owner JWT
is_platform_owner:trueorg:nullservice:null- Used for platform administration endpoints
2. Organization Management JWT
is_platform_owner:falseorg:"organization-slug"service:null- Used for organization management endpoints
3. Service JWT
is_platform_owner:falseorg:"organization-slug"service:"service-slug"- Used for end-user application access
4. Pre-Auth JWT (MFA flows)
mfa_required:true- Short-lived (10 minutes)
- Cannot access protected resources
- Must be exchanged via
/api/auth/mfa/verify
Security Considerations
Rate Limiting
Authentication endpoints are protected by rate limiting:
- Auth endpoints: 100 requests per 15 minutes per IP
- Device flow endpoints: 20 requests per minute per IP
- MFA verification: 5 attempts per 5 minutes per user
Password Requirements
- Minimum 8 characters
- Hashed using Argon2id
- Passwords never stored in plaintext
- All sessions revoked on password change
OAuth Security
- PKCE (Proof Key for Code Exchange) supported for Microsoft
- State parameter used for CSRF protection
- Redirect URI validation against whitelist
- OAuth states expire after 10 minutes
- One-time use OAuth states (deleted after callback)
Session Security
- JWT tokens are stateless but tracked for revocation
- Refresh token rotation prevents token theft
- Refresh tokens expire after 30 days
- Sessions can be revoked individually or all at once
- Token hashes stored using SHA256
MFA Security
- TOTP secrets encrypted at rest using AES-GCM
- Backup codes hashed using SHA256
- Rate limiting on MFA verification attempts
- Audit logging for all MFA events
- MFA required before device authorization completion
Error Responses
All authentication endpoints follow a consistent error format:
{
"error": "Human-readable error message",
"error_code": "ERROR_CODE_ENUM",
"timestamp": "2025-01-15T10:30:00Z"
}
Common Error Codes
| HTTP Status | Error Code | Description |
|---|---|---|
400 Bad Request |
BAD_REQUEST |
Invalid request parameters |
400 Bad Request |
DEVICE_CODE_EXPIRED |
Device code has expired |
401 Unauthorized |
UNAUTHORIZED |
Invalid credentials |
401 Unauthorized |
TOKEN_EXPIRED |
JWT has expired |
401 Unauthorized |
JWT_ERROR |
Invalid JWT signature |
403 Forbidden |
FORBIDDEN |
Insufficient permissions |
403 Forbidden |
ORGANIZATION_NOT_ACTIVE |
Organization suspended |
404 Not Found |
NOT_FOUND |
Resource not found |
429 Too Many Requests |
RATE_LIMIT_EXCEEDED |
Rate limit exceeded |
500 Internal Server Error |
INTERNAL_SERVER_ERROR |
Server error |
500 Internal Server Error |
OAUTH_ERROR |
OAuth provider error |
Complete Authentication Flows
Standard OAuth Flow
- Frontend redirects to
GET /auth/:provider?org=X&service=Y&redirect_uri=Z - User authenticates with OAuth provider
- Provider redirects to
GET /auth/:provider/callback?code=ABC&state=XYZ - Backend exchanges code for tokens, creates/updates user
- User redirected to
redirect_uri?access_token=JWT&refresh_token=UUID - Frontend stores tokens and makes authenticated requests
Password + MFA Flow
- User submits
POST /api/auth/loginwith email/password - If MFA enabled, receive
{"access_token": "preauth_...", "mfa_required": true} - Prompt user for MFA code
- Submit
POST /api/auth/mfa/verifywith preauth token and code - Receive full access token and refresh token
- Store tokens and make authenticated requests
Device Flow
- CLI calls
POST /auth/device/code→ receivesuser_codeandverification_uri - CLI displays: “Visit {verification_uri} and enter code {user_code}”
- User visits URL in browser, enters code
- Frontend calls
POST /auth/device/verify→ gets org/service context - User selects OAuth provider and authenticates
- Backend associates user with device code
- CLI polls
POST /auth/tokenevery 5 seconds - Once authorized, CLI receives access token
Token Refresh Flow
- Access token expires (after 24 hours)
- Client calls
POST /api/auth/refreshwith refresh token - Receive new access token AND new refresh token
- Replace both tokens in storage
- Continue making authenticated requests with new access token
Best Practices
Token Storage
- Web: Store tokens in memory or httpOnly cookies (never localStorage for sensitive apps)
- Mobile: Use secure keychain/keystore
- CLI: Use encrypted credential storage
Error Handling
- Always check for
401status and refresh token automatically - Redirect to login on
403or expired refresh token - Handle rate limiting with exponential backoff
- Display user-friendly error messages
Security
- Always use HTTPS in production
- Validate redirect URIs server-side
- Implement CSRF protection for OAuth flows
- Use refresh token rotation
- Revoke sessions on sensitive operations (password change, etc.)
- Enable MFA for administrative accounts