Authentication API

Comprehensive authentication endpoints including registration, login, OAuth flows, device authorization, JWT management, and session handling for both admins and end-users.

Updated Dec 16, 2025
Edit on GitHub
authentication oauth2 jwt device-flow registration

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 found
  • 400 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:

  1. CLI displays user_code and verification_uri to user
  2. User visits verification_uri in browser and enters user_code
  3. CLI polls /auth/token endpoint every 5 seconds using device_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 interval from /auth/device/code)
  • Continue polling until you receive an access token or error
  • Not authorized means user hasn’t authorized yet
  • Stop polling on any other error

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_LIMITING env 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 challenge
  • 401 Unauthorized: Challenge doesn’t belong to user
  • 500 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 challenge
  • 403 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: true
  • org: null
  • service: null
  • Used for platform administration endpoints

2. Organization Management JWT

  • is_platform_owner: false
  • org: "organization-slug"
  • service: null
  • Used for organization management endpoints

3. Service JWT

  • is_platform_owner: false
  • org: "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

  1. Frontend redirects to GET /auth/:provider?org=X&service=Y&redirect_uri=Z
  2. User authenticates with OAuth provider
  3. Provider redirects to GET /auth/:provider/callback?code=ABC&state=XYZ
  4. Backend exchanges code for tokens, creates/updates user
  5. User redirected to redirect_uri?access_token=JWT&refresh_token=UUID
  6. Frontend stores tokens and makes authenticated requests

Password + MFA Flow

  1. User submits POST /api/auth/login with email/password
  2. If MFA enabled, receive {"access_token": "preauth_...", "mfa_required": true}
  3. Prompt user for MFA code
  4. Submit POST /api/auth/mfa/verify with preauth token and code
  5. Receive full access token and refresh token
  6. Store tokens and make authenticated requests

Device Flow

  1. CLI calls POST /auth/device/code → receives user_code and verification_uri
  2. CLI displays: “Visit {verification_uri} and enter code {user_code}”
  3. User visits URL in browser, enters code
  4. Frontend calls POST /auth/device/verify → gets org/service context
  5. User selects OAuth provider and authenticates
  6. Backend associates user with device code
  7. CLI polls POST /auth/token every 5 seconds
  8. Once authorized, CLI receives access token

Token Refresh Flow

  1. Access token expires (after 24 hours)
  2. Client calls POST /api/auth/refresh with refresh token
  3. Receive new access token AND new refresh token
  4. Replace both tokens in storage
  5. 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 401 status and refresh token automatically
  • Redirect to login on 403 or 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