arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 68

Authority

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

How-To Guides

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Reference

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Explanation

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Rate Limits

Authority implements rate limiting to protect against abuse.

hashtag
Default Limits

Endpoint
Limit
Window

/authorize

hashtag
Response Headers

Rate limit information is included in response headers:

Header
Description

hashtag
Rate Limit Exceeded

When rate limit is exceeded:

hashtag
Rate Limit Types

hashtag
Per IP Address

Default rate limiting is per IP address:

hashtag
Per Client

Rate limit by OAuth client:

hashtag
Per User

Rate limit by authenticated user:

hashtag
Configuration

hashtag
Environment Variables

Variable
Default
Description

hashtag
Per-Endpoint Configuration

hashtag
Whitelist

Exclude IPs from rate limiting:

hashtag
Client-Specific Limits

Configure different limits per client:

hashtag
Best Practices

hashtag
Client Implementation

  1. Check headers - Monitor X-RateLimit-Remaining

  2. Implement backoff - Use exponential backoff on 429

  3. Cache tokens - Reduce token requests

hashtag
Handling Rate Limits

hashtag
Monitoring

Monitor rate limit metrics:

  • Rate of 429 responses

  • Clients hitting limits frequently

  • Unusual traffic patterns

hashtag
Redis-Based Rate Limiting

For distributed deployments, use Redis:

This ensures consistent rate limiting across multiple Authority instances.

hashtag
Next Steps

  • - Endpoint reference

  • - Error handling

  • - Redis setup

Batch requests - Combine when possible

60

1 minute

/token

60

1 minute

/oauth2/userinfo

120

1 minute

/register

10

1 minute

/signin

10

1 minute

/forgot-password

3

1 hour

X-RateLimit-Limit

Maximum requests in window

X-RateLimit-Remaining

Requests remaining

X-RateLimit-Reset

Unix timestamp when limit resets

RATE_LIMIT_ENABLED

true

Enable rate limiting

RATE_LIMIT_BY

ip

Rate limit key

RATE_LIMIT_WINDOW

60

Window in seconds

RATE_LIMIT_MAX

60

Max requests per window

API Endpoints
Error Codes
Redis Caching
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1699999999
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{
  "error": "rate_limit_exceeded",
  "error_description": "Too many requests. Please retry after 60 seconds.",
  "retry_after": 60
}
RATE_LIMIT_BY=ip
RATE_LIMIT_BY=client
RATE_LIMIT_BY=user
# Token endpoint: 60 requests per minute
RATE_LIMIT_TOKEN_MAX=60
RATE_LIMIT_TOKEN_WINDOW=60

# Login endpoint: 10 requests per minute
RATE_LIMIT_LOGIN_MAX=10
RATE_LIMIT_LOGIN_WINDOW=60

# Password reset: 3 requests per hour
RATE_LIMIT_PASSWORD_RESET_MAX=3
RATE_LIMIT_PASSWORD_RESET_WINDOW=3600
RATE_LIMIT_WHITELIST=10.0.0.0/8,192.168.1.0/24
{
  "client_id": "trusted-client",
  "rate_limits": {
    "token": {
      "max": 1000,
      "window": 60
    }
  }
}
async function makeRequest(url, options, retries = 3) {
  const response = await fetch(url, options);

  if (response.status === 429) {
    if (retries > 0) {
      const retryAfter = response.headers.get('Retry-After') || 60;
      await sleep(retryAfter * 1000);
      return makeRequest(url, options, retries - 1);
    }
    throw new Error('Rate limit exceeded');
  }

  return response;
}
RATE_LIMIT_STORE=redis
REDIS_URL=redis://localhost:6379

Social Login

API endpoints for social authentication (OAuth federation).

hashtag
Endpoints Overview

Method
Endpoint
Description

GET

hashtag
Supported Providers

Provider
Identifier

hashtag
Initiate Social Login

Start the OAuth flow with a social provider.

hashtag
Path Parameters

Parameter
Type
Description

hashtag
Query Parameters

Parameter
Type
Required
Description

hashtag
Response

Redirects to the provider's authorization page.

hashtag
Example

hashtag
Errors

Status
Error
Description

hashtag
OAuth Callback

Handles the callback from the social provider after user authorization.

hashtag
Path Parameters

Parameter
Type
Description

hashtag
Query Parameters

Parameter
Type
Description

hashtag
Response

On success, redirects to:

  • The forward_url if provided during initiation

  • The default post-login page otherwise

Sets session cookie for authenticated user.

hashtag
Errors

Status
Error
Description

hashtag
Unlink Social Account

Remove a linked social account from the authenticated user.

hashtag
Path Parameters

Parameter
Type
Description

hashtag
Authentication

Requires active session (session cookie).

hashtag
Response

Success: 302 redirect to /profile with success flash message.

Error: 302 redirect to /profile with error flash message.

hashtag
Errors

Status
Error
Description

hashtag
Example


hashtag
User Data from Providers

Each provider returns different user information.

hashtag
Google

hashtag
GitHub

hashtag
Facebook

hashtag
LinkedIn

hashtag
Apple

circle-info

Apple only provides the user's name on the first authentication.


hashtag
State Parameter

The state parameter provides CSRF protection:

  1. Authority generates a random state value

  2. State is stored server-side with expiration

  3. State is included in the authorization URL

  4. Provider returns the state in the callback

State tokens expire after 10 minutes.


hashtag
Session Handling

After successful authentication:

  1. User record created or updated

  2. Social connection record created/updated

  3. Session created for user

  4. Session cookie set in response

Session cookie attributes:

  • HttpOnly: Yes

  • Secure: Yes (in production)

  • SameSite: Lax


hashtag
Configuration Settings

Social login is configured via settings:

Setting
Description

hashtag
Next Steps

  • - Setup guide

  • - Account linking

  • - All API endpoints

  • Authority validates state matches stored value

  • User redirected to application

  • FACEBOOK_OAUTH_ENABLED

    Enable Facebook provider

    FACEBOOK_CLIENT_ID

    Facebook app ID

    FACEBOOK_CLIENT_SECRET

    Facebook app secret

    LINKEDIN_OAUTH_ENABLED

    Enable LinkedIn provider

    LINKEDIN_CLIENT_ID

    LinkedIn client ID

    LINKEDIN_CLIENT_SECRET

    LinkedIn client secret

    APPLE_OAUTH_ENABLED

    Enable Apple provider

    APPLE_CLIENT_ID

    Apple Services ID

    APPLE_TEAM_ID

    Apple Team ID

    APPLE_KEY_ID

    Apple Key ID

    APPLE_PRIVATE_KEY

    Apple private key (PEM format)

    /auth/{provider}

    Initiate social login

    GET

    /auth/{provider}/callback

    OAuth callback handler

    POST

    /auth/{provider}/unlink

    Unlink social account

    Google

    google

    GitHub

    github

    Facebook

    facebook

    LinkedIn

    linkedin

    Apple

    apple

    provider

    string

    Provider identifier (google, github, etc.)

    forward_url

    string

    No

    Base64-encoded URL to redirect after auth

    400

    invalid_provider

    Provider not recognized

    400

    provider_disabled

    Provider not enabled in settings

    provider

    string

    Provider identifier

    code

    string

    Authorization code from provider

    state

    string

    CSRF protection state parameter

    error

    string

    Error code if authorization failed

    error_description

    string

    Human-readable error message

    400

    invalid_state

    State parameter invalid or expired

    400

    authorization_denied

    User denied authorization

    400

    invalid_code

    Authorization code invalid or expired

    500

    provider_error

    Error communicating with provider

    provider

    string

    Provider identifier to unlink

    400

    invalid_provider

    Provider not recognized

    400

    not_linked

    User doesn't have this provider linked

    400

    cannot_unlink

    Would leave account without login method

    401

    unauthorized

    Not authenticated

    GOOGLE_OAUTH_ENABLED

    Enable Google provider

    GOOGLE_CLIENT_ID

    Google OAuth client ID

    GOOGLE_CLIENT_SECRET

    Google OAuth client secret

    GITHUB_OAUTH_ENABLED

    Enable GitHub provider

    GITHUB_CLIENT_ID

    GitHub OAuth client ID

    GITHUB_CLIENT_SECRET

    GitHub OAuth client secret

    Configure Google
    Manage Linked Accounts
    Endpoints
    GET /auth/{provider}
    # Basic social login
    curl -I "https://auth.example.com/auth/google"
    
    # With redirect URL
    curl -I "https://auth.example.com/auth/google?forward_url=aHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20vZGFzaGJvYXJk"
    GET /auth/{provider}/callback
    POST /auth/{provider}/unlink
    curl -X POST "https://auth.example.com/auth/google/unlink" \
      -H "Cookie: session=..." \
      -H "Content-Type: application/json"
    {
      "sub": "google-user-id",
      "email": "user@gmail.com",
      "email_verified": true,
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "picture": "https://lh3.googleusercontent.com/..."
    }
    {
      "id": 12345678,
      "login": "johndoe",
      "email": "john@example.com",
      "name": "John Doe",
      "avatar_url": "https://avatars.githubusercontent.com/..."
    }
    {
      "id": "facebook-user-id",
      "email": "john@example.com",
      "name": "John Doe",
      "picture": {
        "data": {
          "url": "https://graph.facebook.com/..."
        }
      }
    }
    {
      "sub": "linkedin-member-id",
      "email": "john@example.com",
      "email_verified": true,
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "picture": "https://media.licdn.com/..."
    }
    {
      "sub": "apple-user-id",
      "email": "john@example.com",
      "email_verified": true,
      "name": "John Doe"
    }

    Device Flow

    The device authorization flow enables OAuth on devices that have limited input capabilities, such as smart TVs, IoT devices, and CLIs.

    hashtag
    Overview

    This flow allows devices without a browser to authenticate users by directing them to complete authorization on a secondary device (phone or computer).

    hashtag
    Use Cases

    • Smart TVs

    • Media consoles

    • Picture frames

    • Printers

    hashtag
    Flow Diagram

    hashtag
    Device Authorization Request

    POST /device

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Response

    Field
    Description

    hashtag
    Display to User

    Show the user:

    Or display a QR code linking to verification_uri_complete.

    hashtag
    Token Polling

    POST /token

    Poll the token endpoint while waiting for user authorization.

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Responses

    Authorization Pending:

    Slow Down (polling too fast):

    Success:

    Access Denied:

    Expired Token:

    hashtag
    Complete Example

    hashtag
    CLI Application (Node.js)

    hashtag
    Python CLI

    hashtag
    User Experience

    The activation page shows:

    After entering the code, users see the standard consent screen.

    hashtag
    Security Considerations

    circle-exclamation
    • Protect device_code - It's equivalent to an authorization code

    • Respect interval - Don't poll faster than specified

    hashtag
    Next Steps

    • - Response format

    • - Token renewal

    • - Error handling

    Client Credentials

    The client credentials grant is used for machine-to-machine authentication where no user is involved.

    hashtag
    Overview

    This flow is for server-side applications that need to access resources on their own behalf, not on behalf of a user.

    hashtag
    Use Cases

    • Background jobs

    • Microservice communication

    • API integrations

    • Scheduled tasks

    hashtag
    Flow Diagram

    hashtag
    Token Request

    POST /token

    hashtag
    Headers

    Header
    Value

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Response

    circle-info

    Client credentials grant does not return a refresh token since the client can always request a new token.

    hashtag
    Authentication Methods

    hashtag
    HTTP Basic Authentication (Recommended)

    hashtag
    POST Body

    hashtag
    Complete Examples

    hashtag
    Node.js

    hashtag
    Python

    hashtag
    curl

    hashtag
    Token Caching

    Since client credentials tokens have no refresh token, cache them:

    hashtag
    Scopes

    Clients can only request scopes they're authorized for:

    hashtag
    Security Considerations

    circle-exclamation
    • Protect credentials - Store client secret securely

    • Use short token lifetimes - Minimize exposure window

    hashtag
    Differences from Other Grants

    Aspect
    Client Credentials
    Authorization Code

    hashtag
    Next Steps

    • - Token renewal

    • - Response format

    • - Secret management

    CLI applications

  • IoT devices

  • Handle expiration - Start over if codes expire
  • Use HTTPS - All communication must be encrypted

  • client_id

    Yes

    Client identifier

    scope

    Optional

    Requested scopes

    device_code

    Code for token polling (keep secret)

    user_code

    Code user enters (6-8 characters)

    verification_uri

    URL user visits

    verification_uri_complete

    URL with code pre-filled

    expires_in

    Seconds until codes expire

    interval

    Minimum polling interval

    grant_type

    Yes

    urn:ietf:params:oauth:grant-type:device_code

    device_code

    Yes

    Device code from initial response

    client_id

    Yes

    Client identifier

    Token Response
    Refresh Tokens
    Error Codes

    System maintenance scripts

    Rotate secrets regularly - Limit compromise impact
  • Limit scopes - Request only needed permissions

  • Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    Must be client_credentials

    scope

    Optional

    Space-separated scopes

    User involved

    No

    Yes

    Refresh token

    No

    Yes

    Use case

    Service-to-service

    User authentication

    Client type

    Confidential only

    Refresh Tokens
    Token Response
    Rotate Secrets

    Both

    POST /device HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
    
    client_id=cli_client&scope=openid%20profile
    {
      "device_code": "e2623df1-8594-47b4-b528-41ed3daecc1a",
      "user_code": "WDJB-MJHT",
      "verification_uri": "https://auth.example.com/activate",
      "verification_uri_complete": "https://auth.example.com/activate?user_code=WDJB-MJHT",
      "expires_in": 300,
      "interval": 5
    }
    To sign in, visit:
    
      https://auth.example.com/activate
    
    And enter the code:
    
      WDJB-MJHT
    
    Waiting for authorization...
    POST /token HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=urn:ietf:params:oauth:grant-type:device_code
    &device_code=e2623df1-8594-47b4-b528-41ed3daecc1a
    &client_id=cli_client
    {
      "error": "authorization_pending",
      "error_description": "The user has not yet completed authorization"
    }
    {
      "error": "slow_down",
      "error_description": "Polling too frequently"
    }
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "scope": "openid profile"
    }
    {
      "error": "access_denied",
      "error_description": "The user denied authorization"
    }
    {
      "error": "expired_token",
      "error_description": "The device code has expired"
    }
    const CLIENT_ID = 'cli_client';
    const AUTHORITY_URL = 'https://auth.example.com';
    
    async function login() {
      // Step 1: Get device code
      const deviceResponse = await fetch(`${AUTHORITY_URL}/device`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
          client_id: CLIENT_ID,
          scope: 'openid profile email'
        })
      });
    
      const device = await deviceResponse.json();
    
      // Step 2: Display to user
      console.log('\nTo sign in, visit:');
      console.log(`  ${device.verification_uri}`);
      console.log('\nAnd enter the code:');
      console.log(`  ${device.user_code}`);
      console.log('\nWaiting for authorization...\n');
    
      // Step 3: Poll for token
      const interval = device.interval * 1000;
      const expiresAt = Date.now() + (device.expires_in * 1000);
    
      while (Date.now() < expiresAt) {
        await sleep(interval);
    
        const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
            device_code: device.device_code,
            client_id: CLIENT_ID
          })
        });
    
        const result = await tokenResponse.json();
    
        if (result.access_token) {
          console.log('Login successful!');
          return result;
        }
    
        if (result.error === 'authorization_pending') {
          process.stdout.write('.');
          continue;
        }
    
        if (result.error === 'slow_down') {
          await sleep(5000); // Additional delay
          continue;
        }
    
        throw new Error(result.error_description || result.error);
      }
    
      throw new Error('Device code expired');
    }
    
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    // Run
    login()
      .then(tokens => console.log('Access token:', tokens.access_token))
      .catch(err => console.error('Error:', err.message));
    import requests
    import time
    
    CLIENT_ID = 'cli_client'
    AUTHORITY_URL = 'https://auth.example.com'
    
    def login():
        # Step 1: Get device code
        response = requests.post(
            f'{AUTHORITY_URL}/device',
            data={
                'client_id': CLIENT_ID,
                'scope': 'openid profile email'
            }
        )
        device = response.json()
    
        # Step 2: Display to user
        print(f"\nTo sign in, visit:\n  {device['verification_uri']}")
        print(f"\nAnd enter the code:\n  {device['user_code']}")
        print("\nWaiting for authorization...")
    
        # Step 3: Poll for token
        interval = device['interval']
        expires_at = time.time() + device['expires_in']
    
        while time.time() < expires_at:
            time.sleep(interval)
    
            response = requests.post(
                f'{AUTHORITY_URL}/token',
                data={
                    'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
                    'device_code': device['device_code'],
                    'client_id': CLIENT_ID
                }
            )
    
            result = response.json()
    
            if 'access_token' in result:
                print("\nLogin successful!")
                return result
    
            if result.get('error') == 'authorization_pending':
                print('.', end='', flush=True)
                continue
    
            if result.get('error') == 'slow_down':
                time.sleep(5)
                continue
    
            raise Exception(result.get('error_description', result.get('error')))
    
        raise Exception('Device code expired')
    
    if __name__ == '__main__':
        tokens = login()
        print(f"Access token: {tokens['access_token']}")
    Enter the code displayed on your device:
    
    ┌─────────────────────────────────┐
    │          WDJB-MJHT              │
    └─────────────────────────────────┘
    
                    [Continue]
    POST /token HTTP/1.1
    Host: auth.example.com
    Authorization: Basic YWJjMTIzOnNlY3JldA==
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials&scope=read%20write
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "read write"
    }
    Authorization: Basic base64(client_id:client_secret)
    POST /token HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials
    &client_id=abc123
    &client_secret=your_secret
    const CLIENT_ID = 'your_client_id';
    const CLIENT_SECRET = 'your_client_secret';
    const AUTHORITY_URL = 'https://auth.example.com';
    
    async function getAccessToken() {
      const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
    
      const response = await fetch(`${AUTHORITY_URL}/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Authorization': `Basic ${credentials}`
        },
        body: new URLSearchParams({
          grant_type: 'client_credentials',
          scope: 'read write'
        })
      });
    
      const data = await response.json();
      return data.access_token;
    }
    
    // Use the token
    async function callApi() {
      const token = await getAccessToken();
    
      const response = await fetch('https://api.example.com/data', {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      });
    
      return response.json();
    }
    import requests
    from requests.auth import HTTPBasicAuth
    
    CLIENT_ID = 'your_client_id'
    CLIENT_SECRET = 'your_client_secret'
    AUTHORITY_URL = 'https://auth.example.com'
    
    def get_access_token():
        response = requests.post(
            f'{AUTHORITY_URL}/token',
            data={
                'grant_type': 'client_credentials',
                'scope': 'read write'
            },
            auth=HTTPBasicAuth(CLIENT_ID, CLIENT_SECRET)
        )
    
        return response.json()['access_token']
    
    # Use the token
    def call_api():
        token = get_access_token()
    
        response = requests.get(
            'https://api.example.com/data',
            headers={'Authorization': f'Bearer {token}'}
        )
    
        return response.json()
    # Get token
    TOKEN=$(curl -s -X POST https://auth.example.com/token \
      -u "client_id:client_secret" \
      -d "grant_type=client_credentials" \
      -d "scope=read" \
      | jq -r '.access_token')
    
    # Use token
    curl https://api.example.com/data \
      -H "Authorization: Bearer $TOKEN"
    class TokenCache {
      constructor() {
        this.token = null;
        this.expiresAt = null;
      }
    
      async getToken() {
        // Return cached token if still valid
        if (this.token && Date.now() < this.expiresAt - 60000) {
          return this.token;
        }
    
        // Get new token
        const response = await fetchToken();
        this.token = response.access_token;
        this.expiresAt = Date.now() + (response.expires_in * 1000);
    
        return this.token;
      }
    }
    
    const tokenCache = new TokenCache();
    # Request specific scopes
    grant_type=client_credentials&scope=read%20write
    
    # If scope omitted, uses client's default scopes
    grant_type=client_credentials

    Token Response

    Reference for OAuth 2.0 token endpoint responses.

    hashtag
    Successful Response

    hashtag
    Response Fields

    Field
    Type
    Description

    hashtag
    Access Token Format

    Authority issues JWTs as access tokens:

    hashtag
    Decoded JWT

    Header:

    Payload:

    hashtag
    JWT Claims

    Claim
    Description

    hashtag
    Token Types by Grant

    hashtag
    Authorization Code

    hashtag
    Client Credentials

    No refresh token or ID token for client credentials.

    hashtag
    Refresh Token

    New refresh token issued (rotation).

    hashtag
    Bearer Token Usage

    Use the access token in API requests:

    hashtag
    Authorization Header (Recommended)

    hashtag
    Query Parameter (Not Recommended)

    circle-exclamation

    Query parameter usage exposes tokens in logs and browser history.

    hashtag
    Error Response

    See for all error types.

    hashtag
    Token Validation

    hashtag
    JWT Validation Steps

    1. Parse the JWT

    2. Verify signature using JWKS

    3. Check issuer matches Authority URL

    hashtag
    Example Validation

    hashtag
    Token Introspection

    For opaque tokens or real-time validation:

    Response:

    hashtag
    Caching

    HTTP headers prevent caching:

    Never cache token responses.

    hashtag
    Next Steps

    • - Token renewal

    • - Error handling

    • - Conceptual overview

    HTTP/1.1 200 OK
    Content-Type: application/json
    Cache-Control: no-store
    
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "scope": "openid profile email",
      "id_token": "eyJhbGciOiJSUzI1NiIs..."
    }

    String

    Granted scopes (space-separated)

    id_token

    String

    OIDC ID token (when openid scope)

    client_id

    Client identifier

    Check audience matches your client ID
  • Check expiration is in the future

  • Check scope includes required permissions

  • access_token

    String

    The access token

    token_type

    String

    Always Bearer

    expires_in

    Integer

    Token lifetime in seconds

    refresh_token

    String

    Token for renewal (optional)

    iss

    Issuer (Authority URL)

    sub

    Subject (user ID)

    aud

    Audience (client ID)

    exp

    Expiration time

    iat

    Issued at time

    scope

    Granted scopes

    Error Codes
    Refresh Tokens
    Error Codes
    Token Lifecycle

    scope

    eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eS1rZXktMSJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLXV1aWQiLCJhdWQiOiJhYmMxMjMiLCJleHAiOjE2OTk5OTk5OTksImlhdCI6MTY5OTk5NjM5OSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.signature
    {
      "alg": "RS256",
      "typ": "JWT",
      "kid": "authority-key-1"
    }
    {
      "iss": "https://auth.example.com",
      "sub": "user-uuid",
      "aud": "abc123",
      "exp": 1699999999,
      "iat": 1699996399,
      "scope": "openid profile email",
      "client_id": "abc123"
    }
    {
      "access_token": "...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "...",
      "scope": "openid profile email",
      "id_token": "..."
    }
    {
      "access_token": "...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "read write"
    }
    {
      "access_token": "...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "...",
      "scope": "openid profile email"
    }
    GET /api/resource HTTP/1.1
    Host: api.example.com
    Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
    GET /api/resource?access_token=eyJhbGciOiJSUzI1NiIs...
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    
    {
      "error": "invalid_grant",
      "error_description": "The authorization code has expired"
    }
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'https://auth.example.com/.well-known/jwks.json'
    });
    
    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        callback(err, key?.getPublicKey());
      });
    }
    
    function validateToken(token) {
      return new Promise((resolve, reject) => {
        jwt.verify(token, getKey, {
          algorithms: ['RS256'],
          issuer: 'https://auth.example.com',
          audience: 'your_client_id'
        }, (err, decoded) => {
          if (err) reject(err);
          else resolve(decoded);
        });
      });
    }
    POST /token/introspect HTTP/1.1
    Authorization: Basic YWJjMTIzOnNlY3JldA==
    Content-Type: application/x-www-form-urlencoded
    
    token=eyJhbGciOiJSUzI1NiIs...
    {
      "active": true,
      "client_id": "abc123",
      "username": "user@example.com",
      "scope": "openid profile email",
      "sub": "user-uuid",
      "exp": 1699999999
    }
    Cache-Control: no-store
    Pragma: no-cache

    JWKS

    The JWKS endpoint provides public keys for verifying JWT signatures.

    hashtag
    JWKS Endpoint

    GET /.well-known/jwks.json

    Returns the public keys used to sign tokens.

    hashtag
    Response

    hashtag
    Key Properties

    Property
    Description

    hashtag
    Token Verification

    hashtag
    JavaScript

    hashtag
    Python

    hashtag
    Go

    hashtag
    Key Matching

    JWTs include the key ID in the header:

    Match this kid with the key in JWKS.

    hashtag
    Caching

    JWKS should be cached to avoid excessive requests:

    hashtag
    Key Rotation

    Authority may rotate keys periodically:

    1. New key added to JWKS

    2. New tokens signed with new key

    3. Old key remains for existing token validation

    4. Eventually old key is removed

    Your verification code should:

    • Cache JWKS but refresh periodically

    • Handle multiple keys in the set

    • Match key by kid from token header

    hashtag
    Manual Verification

    hashtag
    Next Steps

    • - Token structure

    • - User claims

    • - Provider configuration

    kty

    Key type (RSA)

    kid

    Key ID (used in JWT header)

    use

    Key usage (sig for signing)

    alg

    Algorithm (RS256)

    n

    RSA modulus (Base64URL)

    e

    RSA exponent (Base64URL)

    ID Tokens
    UserInfo
    Discovery
    {
      "keys": [
        {
          "kty": "RSA",
          "kid": "authority-key-1",
          "use": "sig",
          "alg": "RS256",
          "n": "0vx7agoebGcQSuuPiLJXZpt...",
          "e": "AQAB"
        }
      ]
    }
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'https://auth.example.com/.well-known/jwks.json',
      cache: true,
      cacheMaxAge: 600000, // 10 minutes
      rateLimit: true,
      jwksRequestsPerMinute: 10
    });
    
    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        if (err) {
          callback(err);
          return;
        }
        callback(null, key.getPublicKey());
      });
    }
    
    function verifyToken(token) {
      return new Promise((resolve, reject) => {
        jwt.verify(token, getKey, {
          algorithms: ['RS256'],
          issuer: 'https://auth.example.com'
        }, (err, decoded) => {
          if (err) reject(err);
          else resolve(decoded);
        });
      });
    }
    import jwt
    from jwt import PyJWKClient
    
    jwks_client = PyJWKClient(
        "https://auth.example.com/.well-known/jwks.json"
    )
    
    def verify_token(token):
        signing_key = jwks_client.get_signing_key_from_jwt(token)
    
        return jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            issuer="https://auth.example.com",
            audience="your_client_id"
        )
    import (
        "github.com/golang-jwt/jwt/v5"
        "github.com/MicahParks/keyfunc/v2"
    )
    
    func verifyToken(tokenString string) (*jwt.Token, error) {
        jwks, err := keyfunc.Get("https://auth.example.com/.well-known/jwks.json", keyfunc.Options{})
        if err != nil {
            return nil, err
        }
    
        return jwt.Parse(tokenString, jwks.Keyfunc, jwt.WithIssuer("https://auth.example.com"))
    }
    {
      "alg": "RS256",
      "typ": "JWT",
      "kid": "authority-key-1"
    }
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'https://auth.example.com/.well-known/jwks.json',
      cache: true,
      cacheMaxAge: 600000,  // 10 minutes
      timeout: 30000        // 30 seconds timeout
    });
    const crypto = require('crypto');
    
    async function manualVerify(token) {
      const [headerB64, payloadB64, signatureB64] = token.split('.');
    
      // Decode header to get kid
      const header = JSON.parse(Buffer.from(headerB64, 'base64url'));
    
      // Fetch JWKS
      const response = await fetch('https://auth.example.com/.well-known/jwks.json');
      const jwks = await response.json();
    
      // Find matching key
      const jwk = jwks.keys.find(k => k.kid === header.kid);
      if (!jwk) throw new Error('Key not found');
    
      // Convert JWK to PEM
      const pem = jwkToPem(jwk);
    
      // Verify signature
      const verifier = crypto.createVerify('RSA-SHA256');
      verifier.update(`${headerB64}.${payloadB64}`);
    
      const signature = Buffer.from(signatureB64, 'base64url');
      const isValid = verifier.verify(pem, signature);
    
      if (!isValid) throw new Error('Invalid signature');
    
      return JSON.parse(Buffer.from(payloadB64, 'base64url'));
    }

    Security Model

    Understanding Authority's security architecture and design principles.

    hashtag
    Security Architecture

    hashtag
    Defense in Depth

    Authority implements multiple security layers:

    hashtag
    Authentication Security

    hashtag
    Password Security

    Passwords are:

    1. Hashed - Using bcrypt with cost factor 12

    2. Salted - Unique salt per password

    3. Validated - Against policy requirements

    hashtag
    Multi-Factor Authentication

    TOTP-based MFA provides:

    • Something you know - Password

    • Something you have - Authenticator app

    MFA protects against:

    • Credential stuffing

    • Phishing

    • Password breaches

    hashtag
    Account Lockout

    Progressive lockout:

    Protects against brute-force attacks while limiting denial-of-service impact.

    hashtag
    Token Security

    hashtag
    JWT Signing

    Tokens are signed with RS256:

    • Private key - Signs tokens (server only)

    • Public key - Verifies tokens (anyone)

    • Key rotation - Periodic key changes

    hashtag
    Token Lifetimes

    Token
    Lifetime
    Purpose

    hashtag
    Token Rotation

    Refresh tokens rotate on use:

    If a refresh token is stolen, the attacker races with the legitimate client.

    hashtag
    Token Revocation

    Tokens can be revoked:

    • User logout

    • Password change

    • Admin action

    • Security concern

    hashtag
    Session Security

    hashtag
    Session Binding

    Sessions are bound to:

    • User agent

    • IP address (optional)

    • Expiration time

    hashtag
    Session Limits

    • Absolute timeout - Maximum session lifetime

    • Idle timeout - Inactivity limit

    • Single session - One active session per user (optional)

    hashtag
    OAuth Security

    hashtag
    Redirect URI Validation

    • Exact match required

    • No wildcards

    • HTTPS in production

    • Pre-registered only

    hashtag
    State Parameter

    Prevents CSRF:

    hashtag
    PKCE

    Protects public clients:

    hashtag
    Data Protection

    hashtag
    Sensitive Data

    Data
    Storage
    Access

    hashtag
    Database Security

    • Connection encryption (SSL)

    • Prepared statements (SQL injection prevention)

    • Principle of least privilege

    hashtag
    Audit and Compliance

    hashtag
    Logged Events

    • Authentication attempts (success/failure)

    • Token operations (issue/revoke)

    • Admin actions

    • Configuration changes

    hashtag
    Log Contents

    Each log entry includes:

    • Timestamp

    • Actor (user/client/system)

    • Action

    • Resource

    hashtag
    Retention

    • Default: 90 days

    • Configurable per compliance requirements

    • Export for external systems

    hashtag
    Threat Mitigation

    Threat
    Mitigation

    hashtag
    Best Practices

    hashtag
    Deployment

    1. Always use HTTPS - No exceptions

    2. Use strong secrets - Cryptographically random

    3. Enable MFA - At least for admins

    hashtag
    Client Implementation

    1. Validate tokens - Always verify signatures

    2. Use PKCE - For all public clients

    3. Store securely - No localStorage for sensitive tokens

    hashtag
    Next Steps

    • - Token management details

    • - System design

    • - MFA setup guide

    History-checked - Prevent reuse
    Regular backups

    IP address

  • User agent

  • Changes made

  • Monitor logs - Regular review
  • Keep updated - Apply security patches

  • Handle errors - Don't leak information
  • Implement logout - Revoke tokens properly

  • Authorization Code

    10 minutes

    Minimize interception window

    Access Token

    1 hour

    Limit exposure if stolen

    Refresh Token

    30 days

    User convenience

    ID Token

    1 hour

    Authentication proof

    Passwords

    Hashed (bcrypt)

    Never retrievable

    MFA secrets

    Encrypted

    User + admin

    Client secrets

    Hashed

    Never retrievable

    Tokens

    Database + cache

    Service only

    Credential stuffing

    Lockout, MFA, rate limiting

    Token theft

    Short lifetimes, rotation, binding

    CSRF

    State parameter, SameSite cookies

    Code interception

    PKCE, short code lifetime

    Replay attacks

    Nonce, one-time codes

    Brute force

    Lockout, progressive delay

    Token Lifecycle
    Architecture
    Enable MFA
    ┌─────────────────────────────────────────┐
    │        Network Security (HTTPS)          │
    ├─────────────────────────────────────────┤
    │           Rate Limiting                  │
    ├─────────────────────────────────────────┤
    │         Input Validation                 │
    ├─────────────────────────────────────────┤
    │   Authentication (Password + MFA)        │
    ├─────────────────────────────────────────┤
    │       Session Management                 │
    ├─────────────────────────────────────────┤
    │     Authorization (Scopes)               │
    ├─────────────────────────────────────────┤
    │         Audit Logging                    │
    └─────────────────────────────────────────┘
    Attempt 1-4:   Normal login
    Attempt 5:     Account locked (30 min)
    Attempt 6+:    Lock timer resets
    Request with Refresh Token A
    ↓
    Issue new Access Token
    Issue new Refresh Token B
    Invalidate Refresh Token A
    Client generates: state=abc123
    Client sends in request
    Authority returns: state=abc123
    Client verifies match
    code_verifier = random()
    code_challenge = SHA256(code_verifier)
    
    Authorization: Send code_challenge
    Token exchange: Prove with code_verifier

    Quick Start

    Get Authority running in 5 minutes. By the end, you'll have a working OAuth 2.0 server.

    hashtag
    Prerequisites

    • Dockerarrow-up-right installed

    • installed

    hashtag
    Step 1: Clone the Repository

    hashtag
    Step 2: Start Authority

    This starts:

    • Authority server on port 4000

    • PostgreSQL database on port 5432

    hashtag
    Step 3: Access the Dashboard

    Open your browser to .

    You should see the Authority landing page:

    hashtag
    Step 4: Sign In

    Click Sign In and use the default admin credentials:

    • Username: admin@example.com

    • Password: password123

    hashtag
    Step 5: Explore the Admin Dashboard

    After signing in, you can:

    • Manage OAuth Clients - Register applications

    • Manage Users - Create and edit accounts

    • Configure Scopes - Define access permissions

    hashtag
    Step 6: Create Your First OAuth Client

    1. Navigate to OAuth Clients

    2. Click New Client

    3. Fill in:

    You'll receive a client_id and client_secret. Save these for the next tutorial.

    hashtag
    Next Steps

    • - Build a complete OAuth flow

    • - Production deployment options

    • - Configuration options

    hashtag
    Troubleshooting

    hashtag
    Port 4000 is in use

    Stop the conflicting service or change the port:

    hashtag
    Database connection failed

    Ensure PostgreSQL is running:

    hashtag
    Can't access the dashboard

    Check Authority logs:

    View Audit Logs - Track all actions
  • Adjust Settings - Configure security policies

  • Name: My Test App
  • Redirect URI: http://localhost:3000/callback

  • Click Create

  • Docker Composearrow-up-right
    http://localhost:4000arrow-up-right
    First OAuth Integration
    Docker Installation Guide
    Environment Variables
    Landing Page
    Sign In
    Admin Dashboard
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    docker-compose up -d
    PORT=4001 docker-compose up -d
    docker-compose logs db
    docker-compose logs authority

    Choosing Grant Types

    A guide to selecting the right OAuth 2.0 grant type for your application.

    hashtag
    Decision Flowchart

    hashtag
    Quick Reference

    Scenario
    Grant Type
    Security

    hashtag
    Authorization Code

    hashtag
    When to Use

    • Web applications with server-side code

    • You can securely store a client secret

    • Users need to authenticate

    hashtag
    How It Works

    The backend exchanges the code for tokens, keeping secrets secure.

    hashtag
    Example Scenarios

    • Traditional web applications (Rails, Django, Express)

    • Server-rendered applications

    • Applications with session-based auth

    hashtag
    Considerations

    • Requires backend infrastructure

    • Client secret must be protected

    • Full OAuth security guarantees

    hashtag
    Authorization Code + PKCE

    hashtag
    When to Use

    • Single-page applications (React, Vue, Angular)

    • Mobile applications (iOS, Android)

    • Desktop applications

    hashtag
    How It Works

    hashtag
    Example Scenarios

    • React/Vue/Angular SPAs

    • React Native / Flutter apps

    • Electron desktop apps

    • Browser extensions

    hashtag
    Considerations

    • No client secret required

    • Proof of possession via PKCE

    • Recommended for all new public clients

    hashtag
    Client Credentials

    hashtag
    When to Use

    • Backend services calling APIs

    • Microservice communication

    • Scheduled jobs / cron tasks

    • No user context needed

    hashtag
    How It Works

    hashtag
    Example Scenarios

    • Payment processing service

    • Data synchronization jobs

    • Inter-service authentication

    • API integrations

    hashtag
    Considerations

    • No user involved

    • No refresh tokens (just request new token)

    • Client must protect its credentials

    • Scopes limited to service-level access

    hashtag
    Device Code

    hashtag
    When to Use

    • Devices without browsers

    • Limited input capability

    • Smart TVs, game consoles

    • CLI applications

    hashtag
    How It Works

    hashtag
    Example Scenarios

    • Smart TV streaming apps

    • Game console apps

    • CLI tools (gh, aws-cli style)

    • IoT devices

    hashtag
    Considerations

    • User must have secondary device

    • Requires polling mechanism

    • Codes expire (typically 5-15 minutes)

    hashtag
    Password Grant (Legacy)

    hashtag
    When to Use

    circle-exclamation

    Only for first-party, trusted applications where other grants are not feasible.

    • Migrating from legacy systems

    • Highly trusted first-party apps

    • When redirect flow is impossible

    hashtag
    How It Works

    hashtag
    Considerations

    • User enters password in your app

    • App sees user's credentials

    • No consent flow

    • MFA may be bypassed

    hashtag
    Grant Type Comparison

    hashtag
    Security

    Grant
    Secret Protection
    Token Exposure
    Phishing Risk

    hashtag
    User Experience

    Grant
    User Steps
    Complexity
    Best For

    hashtag
    Migration Paths

    hashtag
    From Password to PKCE

    1. Add PKCE support to your app

    2. Implement authorization flow

    3. Migrate users gradually

    4. Disable password grant

    hashtag
    From Implicit to PKCE

    1. Add PKCE code exchange

    2. Update authorization request (response_type=code)

    3. Handle code callback

    hashtag
    Common Mistakes

    hashtag
    Using Implicit Grant

    Don't: Use implicit for new applications Do: Use Authorization Code + PKCE

    hashtag
    Password Grant for Third-Party Apps

    Don't: Let third-party apps collect passwords Do: Use authorization flow with consent

    hashtag
    No PKCE for Public Clients

    Don't: Use auth code without PKCE in SPAs/mobile Do: Always use PKCE for public clients

    hashtag
    Long-Lived Tokens Without Refresh

    Don't: Issue 24-hour access tokens without refresh Do: Short access tokens + refresh tokens

    hashtag
    Next Steps

    • - Protocol fundamentals

    • - Security considerations

    • - Implementation details

    Introduction

    Production-ready OAuth 2.0 Server and OpenID Connect 1.0 Provider

    Authority is a complete authentication infrastructure built with Crystal, featuring enterprise-grade security and a modern admin dashboard.

    hashtag
    Quick Start

    Get Authority running in 5 minutes:

    See Quick Start Tutorial for a complete walkthrough.

    hashtag
    Key Features

    Category
    Features

    hashtag
    Documentation Overview

    This documentation is organized using the :

    hashtag

    Step-by-step guides for learning Authority:

    • - Get running in 5 minutes

    • - Build your first OAuth app

    • - Secure your endpoints

    hashtag

    Task-oriented guides for specific goals:

    • - Docker, source, Kubernetes

    • - Environment setup

    • - MFA, lockout, passwords

    hashtag

    Technical specifications and API documentation:

    • - Grant type specifications

    • - OIDC endpoints

    • - Complete API reference

    hashtag

    Understanding concepts and architecture:

    • - System design

    • - Protocol fundamentals

    • - Security architecture

    hashtag
    Standards Compliance

    Authority implements these specifications:

    • - OAuth 2.0 Authorization Framework

    • - Bearer Token Usage

    • - JSON Web Token (JWT)

    hashtag
    Technology Stack

    Component
    Technology

    hashtag
    Screenshots

    Add Social Login

    Learn how to add "Sign in with Google" to your application in under 10 minutes.

    hashtag
    What You'll Build

    By the end of this tutorial, you'll have:

    • Google OAuth configured in Authority

    • A working "Sign in with Google" button

    • Users automatically created from Google accounts

    hashtag
    Prerequisites

    • Authority running locally or deployed

    • A Google account for testing

    • Basic web development knowledge

    hashtag
    Step 1: Create a Google OAuth App

    1. Go to

    2. Create a new project:

      • Click the project dropdown at the top

    hashtag
    Step 2: Configure Authority

    1. Log in to your Authority admin dashboard at http://localhost:4000/dashboard

    2. Navigate to Settings > Social Login

    3. Find the Google section and configure:

    hashtag
    Step 3: Test the Integration

    1. Open a new incognito/private browser window

    2. Navigate to http://localhost:4000/auth/google

    3. You should be redirected to Google's sign-in page

    hashtag
    Step 4: Add to Your Application

    Add a sign-in button to your app:

    hashtag
    Step 5: Handle the Redirect

    After successful authentication, redirect users to your app:

    After authentication, Authority will redirect users to your specified URL with an active session.

    hashtag
    What Happens Behind the Scenes

    hashtag
    Adding More Providers

    Now that Google is working, add more providers:

    1. GitHub:

    2. Facebook:

    3. LinkedIn:

    hashtag
    Common Issues

    hashtag
    "redirect_uri_mismatch"

    Your callback URL doesn't match Google's configuration.

    Fix: Ensure the redirect URI in Google Console exactly matches:

    hashtag
    "Access blocked: App not verified"

    You're not a test user for an unverified app.

    Fix: In Google Console, go to OAuth consent screen > Test users and add your email.

    hashtag
    User Created But No Session

    Check that your forward_url is correctly encoded.

    Fix: Use btoa() to Base64 encode the URL.

    hashtag
    Next Steps

    • - Let users link multiple providers

    • - Add extra security

    • - Full OAuth client setup

    Getting Started

    Welcome to Authority tutorials. These step-by-step guides will teach you how to use Authority for OAuth 2.0 authentication.

    hashtag
    Available Tutorials

    hashtag
    Quick Start

    Get Authority running in 5 minutes using Docker. You'll have a working OAuth server by the end.

    Prerequisites: Docker installed Duration: 5 minutes

    hashtag

    Build a complete OAuth 2.0 integration from scratch. You'll create a client application, implement the authorization code flow, and access protected resources.

    Prerequisites: Completed Quick Start Duration: 30 minutes

    hashtag

    Learn how to secure your API endpoints using Authority's token validation. Implement access control with scopes.

    Prerequisites: Completed First OAuth Integration Duration: 20 minutes

    hashtag

    Implement user login and registration flows in your application using OpenID Connect.

    Prerequisites: Completed First OAuth Integration Duration: 25 minutes

    hashtag
    What You'll Learn

    By completing these tutorials, you will understand:

    • How to deploy and configure Authority

    • OAuth 2.0 authorization code flow with PKCE

    • Token validation and introspection

    hashtag
    Getting Help

    If you get stuck:

    1. Check the for specific tasks

    2. Review the for technical details

    3. Read the for conceptual understanding

    Add User Authentication

    Implement user login and registration in your application using OpenID Connect.

    hashtag
    Prerequisites

    • Completed First OAuth Integration

    • Understanding of OAuth 2.0 authorization code flow

    hashtag
    What You'll Learn

    • Use OpenID Connect for user authentication

    • Parse and validate ID tokens

    • Display user profile information

    hashtag
    OpenID Connect Overview

    OpenID Connect adds an identity layer on top of OAuth 2.0. Instead of just getting an access token, you also receive an ID token containing user information.

    hashtag
    Step 1: Request OpenID Scopes

    Add openid and profile scopes to your authorization request:

    hashtag
    Available Scopes

    Scope
    Claims Included

    hashtag
    Step 2: Parse the ID Token

    The token response now includes an ID token:

    Decode the ID token (it's a JWT):

    hashtag
    Step 3: Validate the ID Token

    Always validate ID tokens before trusting them:

    hashtag
    Step 4: Fetch Additional User Info

    Use the UserInfo endpoint for more claims:

    hashtag
    Step 5: Implement Session Management

    hashtag
    Store Session Securely

    hashtag
    Display User Profile

    hashtag
    Step 6: Implement Logout

    hashtag
    Client-Side Logout

    hashtag
    RP-Initiated Logout (Optional)

    Authority supports OpenID Connect RP-Initiated Logout:

    hashtag
    Complete Implementation

    hashtag
    Next Steps

    • - Full specification

    • - Token structure

    • - Available claims

    # Clone the repository
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    
    # Start with Docker
    docker-compose up -d
    
    # Visit http://localhost:4000
    OpenID Connect user authentication
  • Scope-based access control

  • First OAuth Integration
    Protect Your API
    Add User Authentication
    How-To Guides
    Reference
    Explanation
    Add User Authentication - Implement login flows
    OAuth Clients - Client management
    Configuration - All settings
    Choosing Grant Types - Decision guide
    RFC 7636arrow-up-right - Proof Key for Code Exchange (PKCE)
  • RFC 7662arrow-up-right - Token Introspection

  • RFC 7009arrow-up-right - Token Revocation

  • RFC 8628arrow-up-right - Device Authorization Grant

  • OpenID Connect Core 1.0arrow-up-right

  • OAuth 2.0

    Authorization Code, PKCE, Client Credentials, Device Flow, Refresh Tokens

    OpenID Connect

    ID Tokens, UserInfo, Discovery, JWKS

    Security

    MFA/TOTP, Account Lockout, Password Policies, Audit Logging

    Admin

    Client Management, User Management, Scope Configuration, Settings

    Language

    Crystal

    Web Framework

    Azu

    Database

    PostgreSQL

    Templating

    Crinja (Jinja2-compatible)

    Caching

    Redis (optional)

    Diataxis frameworkarrow-up-right
    Tutorials
    Quick Start
    First OAuth Integration
    Protect Your API
    How-To Guides
    Installation
    Configuration
    Security
    Reference
    OAuth 2.0 Flows
    OpenID Connect
    API Endpoints
    Explanation
    Architecture
    OAuth 2.0 Concepts
    Security Model
    RFC 6749arrow-up-right
    RFC 6750arrow-up-right
    RFC 7519arrow-up-right
    Landing Page
    Admin Dashboard

    Device Code

    Medium

    Smart TV / IoT

    Device Code

    Medium

    First-party mobile app

    Password (with caution)

    Medium

    Any public client (cannot keep secrets)
  • Printers / picture frames

  • Good UX requires clear instructions
  • Never for third-party apps

  • Device Code

    Medium

    Low

    Medium

    Password

    Low

    Medium

    High

    Device Code

    Visit URL + Enter code

    High

    Limited input

    Password

    Enter credentials

    Low

    Legacy

    Remove fragment token handling

    Web app with backend

    Authorization Code

    High

    Single-page application

    Authorization Code + PKCE

    High

    Mobile application

    Authorization Code + PKCE

    High

    Server-to-server

    Client Credentials

    High

    Auth Code

    High

    Low

    Low

    Auth Code + PKCE

    Medium

    Low

    Low

    Client Credentials

    High

    N/A

    Auth Code

    Login + Consent

    Medium

    Web apps

    Auth Code + PKCE

    Login + Consent

    Medium

    Mobile/SPA

    Client Credentials

    None

    Low

    OAuth 2.0 Concepts
    Security Model
    Authorization Code Reference

    CLI tool

    N/A

    Services

    Click New Project
  • Name it (e.g., "My App Auth")

  • Click Create

  • Enable the OAuth consent screen:

    • Navigate to APIs & Services > OAuth consent screen

    • Select External user type

    • Click Create

    • Fill in required fields:

      • App name: Your app name

      • User support email: Your email

    • Click Save and Continue through remaining steps

  • Create OAuth credentials:

    • Navigate to APIs & Services > Credentials

    • Click Create Credentials > OAuth client ID

    • Application type: Web application

    • Name: "Authority Integration"

    • Authorized redirect URIs: http://localhost:4000/auth/google/callback

    • Click Create

  • Copy your Client ID and Client Secret

  • Toggle Enable Google OAuth to ON

  • Paste your Client ID

  • Paste your Client Secret

  • Click Save

  • Sign in with your Google account
  • After approval, you'll be redirected back to Authority

  • Check the admin dashboard - a new user should appear!

  • Apple: Configure Apple
    Google Cloud Consolearrow-up-right
    Configure GitHub
    Configure Facebook
    Configure LinkedIn
    Manage Linked Accounts
    Enable MFA
    First OAuth Integration
    Handle sessions and logout

    openid

    sub (required for OIDC)

    profile

    name, family_name, given_name, picture

    email

    email, email_verified

    address

    address

    phone

    phone_number, phone_number_verified

    OpenID Connect Reference
    ID Tokens
    UserInfo Endpoint

    Protect Your API

    Learn how to secure your API endpoints using Authority's token validation.

    hashtag
    Prerequisites

    • Completed First OAuth Integration

    • A backend API you want to protect

    hashtag
    What You'll Learn

    • Validate access tokens server-side

    • Implement scope-based access control

    • Handle token expiration gracefully

    hashtag
    Token Validation Approaches

    There are two ways to validate tokens:

    1. Local validation - Verify JWT signature using Authority's public keys

    2. Token introspection - Ask Authority if the token is valid

    hashtag
    When to Use Each

    Approach
    Use When

    hashtag
    Approach 1: Local JWT Validation

    hashtag
    Step 1: Fetch the JWKS

    Authority publishes its public keys at /.well-known/jwks.json:

    hashtag
    Step 2: Validate the Token

    Node.js Example:

    Python Example:

    hashtag
    Step 3: Create Middleware

    Express.js Middleware:

    hashtag
    Approach 2: Token Introspection

    For real-time validation, use the introspection endpoint.

    hashtag
    Step 1: Configure Client Credentials

    Token introspection requires client authentication:

    hashtag
    Step 2: Introspect the Token

    The response:

    If the token is invalid or revoked:

    hashtag
    Scope-Based Access Control

    hashtag
    Define Scopes

    Configure scopes in Authority's admin dashboard:

    Scope
    Description

    hashtag
    Check Scopes in Middleware

    hashtag
    Handling Token Expiration

    hashtag
    Client-Side: Refresh Before Expiry

    hashtag
    Server-Side: Return Clear Errors

    hashtag
    Next Steps

    • - OpenID Connect integration

    • - Full specification

    • - Token renewal

    First OAuth Integration

    Build a complete OAuth 2.0 integration from scratch. You'll implement the authorization code flow with PKCE.

    hashtag
    Prerequisites

    • Completed Quick Start

    • Authority running on http://localhost:4000

    • An OAuth client created with:

      • Redirect URI: http://localhost:3000/callback

    hashtag
    What You'll Build

    A simple web application that:

    1. Redirects users to Authority for authentication

    2. Receives an authorization code

    3. Exchanges the code for access tokens

    4. Uses tokens to access protected resources

    hashtag
    The Authorization Code Flow

    hashtag
    Step 1: Generate PKCE Values

    PKCE protects against authorization code interception. Generate a code verifier and challenge:

    hashtag
    Step 2: Redirect to Authorization Endpoint

    Build the authorization URL and redirect the user:

    hashtag
    Step 3: Handle the Callback

    After the user authenticates, Authority redirects to your callback URL:

    hashtag
    Step 4: Exchange Code for Tokens

    Make a POST request to the token endpoint:

    The response contains:

    hashtag
    Step 5: Use the Access Token

    Include the token in API requests:

    hashtag
    Complete Example

    Here's a minimal HTML page implementing the full flow:

    hashtag
    Next Steps

    • - Validate tokens on your server

    • - Full specification

    • - PKCE details

    Installation

    Deploy Authority using Docker for production environments.

    hashtag
    Prerequisites

    • Docker 20.10+

    • Docker Compose 2.0+

    • 1GB RAM minimum

    • PostgreSQL (included in docker-compose)

    hashtag
    Quick Start

    Authority is now running at http://localhost:4000.

    hashtag
    Production Configuration

    hashtag
    1. Create Environment File

    Create a .env file:

    hashtag
    2. Generate Secret Key

    hashtag
    3. Docker Compose File

    Create docker-compose.yml:

    hashtag
    4. Start Services

    hashtag
    Reverse Proxy Setup

    hashtag
    Nginx Configuration

    hashtag
    Traefik Configuration

    hashtag
    Database Migrations

    Run migrations manually if needed:

    hashtag
    Backup and Restore

    hashtag
    Backup Database

    hashtag
    Restore Database

    hashtag
    Monitoring

    hashtag
    Health Check

    hashtag
    View Logs

    hashtag
    Resource Usage

    hashtag
    Scaling

    For high availability, run multiple Authority instances behind a load balancer:

    circle-exclamation

    When running multiple instances, ensure SECRET_KEY is identical across all instances and use Redis for session storage.

    hashtag
    Troubleshooting

    hashtag
    Container won't start

    Check logs:

    hashtag
    Database connection failed

    Verify PostgreSQL is running:

    hashtag
    Port already in use

    Change the port:

    hashtag
    Next Steps

    • - All configuration options

    • - Enable HTTPS

    • - Session storage

    Database Setup

    Configure PostgreSQL for Authority.

    hashtag
    Requirements

    • PostgreSQL 13 or higher

    • 100MB minimum disk space

    • Recommended: SSD storage

    hashtag
    Creating the Database

    hashtag
    Using psql

    hashtag
    Using createdb

    hashtag
    Connection String

    Configure the DATABASE_URL environment variable:

    hashtag
    SSL Mode

    For production, enable SSL:

    SSL modes:

    • disable - No SSL

    • require - SSL required, no verification

    • verify-ca - Verify server certificate

    hashtag
    Running Migrations

    Migrations create the required tables:

    hashtag
    Tables Created

    Table
    Description

    hashtag
    Seeding Data

    Create initial data:

    This creates:

    • Default admin user

    • Common OAuth scopes

    • System settings

    hashtag
    Backup and Restore

    hashtag
    Backup

    With compression:

    hashtag
    Restore

    From compressed:

    hashtag
    Connection Pooling

    For production, configure connection pooling:

    hashtag
    Using PgBouncer

    Update connection string:

    hashtag
    Performance Tuning

    hashtag
    PostgreSQL Configuration

    hashtag
    Indexes

    Authority creates necessary indexes during migration. For additional performance, consider:

    hashtag
    Troubleshooting

    hashtag
    Connection refused

    Check PostgreSQL is running:

    hashtag
    Authentication failed

    Verify credentials and pg_hba.conf:

    hashtag
    Database does not exist

    Create the database:

    hashtag
    Permission denied

    Grant permissions:

    hashtag
    Next Steps

    • - All configuration options

    • - Session storage

    • - Containerized database

    Kubernetes

    Deploy Authority on Kubernetes for scalable, production-ready authentication.

    hashtag
    Prerequisites

    • Kubernetes cluster (1.24+)

    Configuration

    Configure Authority using environment variables.

    hashtag
    Server Settings

    Variable
    Default
    Description

    From Source

    Build and run Authority from source code.

    hashtag
    Prerequisites

    • 1.9+

    SSL Certificates

    Enable HTTPS for Authority.

    hashtag
    Overview

    There are two approaches to enable HTTPS:

    1. Reverse proxy (recommended) - Terminate SSL at Nginx/Traefik

    Audit Logging

    Track all security-relevant actions in Authority.

    hashtag
    Overview

    Authority logs all significant events to help with:

    • Security monitoring

    Password Policies

    Configure password requirements for Authority users.

    hashtag
    Configuration

    hashtag
    Environment Variables

    User → Your App → Authority → User authenticates → Your Backend → Tokens
    Client generates: code_verifier (random)
    Client sends: code_challenge = SHA256(code_verifier)
    Authority returns: authorization code
    Client proves: code_verifier
    Authority validates: SHA256(code_verifier) == code_challenge
    Authority issues: tokens
    Your Service → Authority (client_id + secret) → Token → Your Service → API
    Device → Authority: Request device code
    Authority → Device: device_code + user_code + URL
    Device → User: "Visit URL, enter code"
    User → Browser → Authority: Enter code, authenticate
    Device → Authority: Poll for token
    Authority → Device: Access token
    App → User: Enter credentials
    App → Authority: username + password
    Authority → App: Tokens
    <!DOCTYPE html>
    <html>
    <head>
      <title>My App</title>
      <style>
        .btn-google {
          display: inline-flex;
          align-items: center;
          padding: 12px 24px;
          background: #4285f4;
          color: white;
          border-radius: 4px;
          text-decoration: none;
          font-family: sans-serif;
          font-weight: 500;
        }
        .btn-google:hover {
          background: #357abd;
        }
        .btn-google svg {
          margin-right: 12px;
        }
      </style>
    </head>
    <body>
      <h1>Welcome to My App</h1>
    
      <a href="http://localhost:4000/auth/google" class="btn-google">
        <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
          <path d="M17.64 9.2c0-.637-.057-1.251-.164-1.84H9v3.481h4.844c-.209 1.125-.843 2.078-1.796 2.717v2.258h2.908c1.702-1.567 2.684-3.874 2.684-6.615z" fill="#4285F4"/>
          <path d="M9.003 18c2.43 0 4.467-.806 5.956-2.18l-2.909-2.26c-.806.54-1.836.86-3.047.86-2.344 0-4.328-1.584-5.036-3.711H.957v2.332A8.997 8.997 0 009.003 18z" fill="#34A853"/>
          <path d="M3.964 10.712A5.41 5.41 0 013.682 9c0-.593.102-1.17.282-1.71V4.958H.957A8.996 8.996 0 000 9c0 1.452.348 2.827.957 4.042l3.007-2.33z" fill="#FBBC05"/>
          <path d="M9.003 3.58c1.321 0 2.508.454 3.44 1.345l2.582-2.58C13.464.891 11.428 0 9.002 0A8.997 8.997 0 00.957 4.958L3.964 7.29c.708-2.127 2.692-3.71 5.036-3.71z" fill="#EA4335"/>
        </svg>
        Sign in with Google
      </a>
    </body>
    </html>
    // Encode your callback URL
    const callbackUrl = 'http://localhost:3000/dashboard';
    const encodedUrl = btoa(callbackUrl);
    
    // Build the auth URL with forward_url
    const authUrl = `http://localhost:4000/auth/google?forward_url=${encodedUrl}`;
    
    // Use this URL for your button
    document.querySelector('.btn-google').href = authUrl;
    http://localhost:4000/auth/google/callback
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: 'openid profile email',  // OpenID Connect scopes
      state: state,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256'
    });
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "id_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "openid profile email"
    }
    function parseIdToken(idToken) {
      const parts = idToken.split('.');
      const payload = JSON.parse(atob(parts[1]));
      return payload;
    }
    
    // Result:
    // {
    //   "iss": "http://localhost:4000",
    //   "sub": "user-uuid",
    //   "aud": "your_client_id",
    //   "exp": 1699999999,
    //   "iat": 1699996399,
    //   "name": "John Doe",
    //   "email": "john@example.com",
    //   "email_verified": true
    // }
    async function validateIdToken(idToken) {
      const payload = parseIdToken(idToken);
    
      // 1. Verify issuer
      if (payload.iss !== AUTHORITY_URL) {
        throw new Error('Invalid issuer');
      }
    
      // 2. Verify audience
      if (payload.aud !== CLIENT_ID) {
        throw new Error('Invalid audience');
      }
    
      // 3. Verify expiration
      if (payload.exp < Date.now() / 1000) {
        throw new Error('Token expired');
      }
    
      // 4. Verify signature (use JWKS)
      await verifySignature(idToken);
    
      return payload;
    }
    async function getUserInfo(accessToken) {
      const response = await fetch(`${AUTHORITY_URL}/userinfo`, {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
    
      if (!response.ok) {
        throw new Error('Failed to fetch user info');
      }
    
      return response.json();
    }
    
    // Result:
    // {
    //   "sub": "user-uuid",
    //   "name": "John Doe",
    //   "given_name": "John",
    //   "family_name": "Doe",
    //   "email": "john@example.com",
    //   "email_verified": true,
    //   "picture": "https://..."
    // }
    class AuthSession {
      constructor() {
        this.tokens = null;
        this.user = null;
      }
    
      login(tokens, user) {
        this.tokens = tokens;
        this.user = user;
    
        // Store refresh token securely (httpOnly cookie recommended)
        localStorage.setItem('auth_session', JSON.stringify({
          accessToken: tokens.access_token,
          expiresAt: Date.now() + (tokens.expires_in * 1000),
          user: user
        }));
      }
    
      getAccessToken() {
        const session = JSON.parse(localStorage.getItem('auth_session'));
        if (!session) return null;
    
        if (Date.now() > session.expiresAt) {
          // Token expired - need to refresh
          return null;
        }
    
        return session.accessToken;
      }
    
      getUser() {
        const session = JSON.parse(localStorage.getItem('auth_session'));
        return session?.user;
      }
    
      logout() {
        localStorage.removeItem('auth_session');
        this.tokens = null;
        this.user = null;
      }
    }
    
    const auth = new AuthSession();
    <div id="profile" style="display: none;">
      <img id="avatar" src="" alt="Profile picture">
      <h2 id="name"></h2>
      <p id="email"></p>
      <button id="logout">Logout</button>
    </div>
    
    <script>
    function showProfile(user) {
      document.getElementById('profile').style.display = 'block';
      document.getElementById('avatar').src = user.picture || '/default-avatar.png';
      document.getElementById('name').textContent = user.name;
      document.getElementById('email').textContent = user.email;
    }
    
    document.getElementById('logout').onclick = () => {
      auth.logout();
      window.location.href = '/';
    };
    </script>
    function logout() {
      auth.logout();
      window.location.href = '/';
    }
    function logoutWithRedirect() {
      auth.logout();
    
      const params = new URLSearchParams({
        post_logout_redirect_uri: 'http://localhost:3000',
        client_id: CLIENT_ID
      });
    
      window.location.href = `${AUTHORITY_URL}/logout?${params}`;
    }
    class AuthClient {
      constructor(config) {
        this.clientId = config.clientId;
        this.redirectUri = config.redirectUri;
        this.authorityUrl = config.authorityUrl;
        this.session = new AuthSession();
      }
    
      async login() {
        const codeVerifier = this.generateCodeVerifier();
        const codeChallenge = await this.generateCodeChallenge(codeVerifier);
        const state = this.generateCodeVerifier();
    
        sessionStorage.setItem('pkce_verifier', codeVerifier);
        sessionStorage.setItem('oauth_state', state);
    
        const params = new URLSearchParams({
          response_type: 'code',
          client_id: this.clientId,
          redirect_uri: this.redirectUri,
          scope: 'openid profile email',
          state: state,
          code_challenge: codeChallenge,
          code_challenge_method: 'S256'
        });
    
        window.location.href = `${this.authorityUrl}/authorize?${params}`;
      }
    
      async handleCallback() {
        const params = new URLSearchParams(window.location.search);
        const code = params.get('code');
        const state = params.get('state');
    
        if (state !== sessionStorage.getItem('oauth_state')) {
          throw new Error('Invalid state');
        }
    
        const codeVerifier = sessionStorage.getItem('pkce_verifier');
        const tokens = await this.exchangeCode(code, codeVerifier);
        const user = await this.getUserInfo(tokens.access_token);
    
        this.session.login(tokens, user);
    
        // Clean up
        sessionStorage.removeItem('pkce_verifier');
        sessionStorage.removeItem('oauth_state');
        window.history.replaceState({}, '', window.location.pathname);
    
        return user;
      }
    
      async exchangeCode(code, codeVerifier) {
        const response = await fetch(`${this.authorityUrl}/token`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: this.redirectUri,
            client_id: this.clientId,
            code_verifier: codeVerifier
          })
        });
        return response.json();
      }
    
      async getUserInfo(accessToken) {
        const response = await fetch(`${this.authorityUrl}/userinfo`, {
          headers: { 'Authorization': `Bearer ${accessToken}` }
        });
        return response.json();
      }
    
      logout() {
        this.session.logout();
      }
    
      isAuthenticated() {
        return this.session.getAccessToken() !== null;
      }
    
      getUser() {
        return this.session.getUser();
      }
    
      // PKCE helpers
      generateCodeVerifier() {
        const array = new Uint8Array(32);
        crypto.getRandomValues(array);
        return this.base64URLEncode(array);
      }
    
      async generateCodeChallenge(verifier) {
        const data = new TextEncoder().encode(verifier);
        const hash = await crypto.subtle.digest('SHA-256', data);
        return this.base64URLEncode(new Uint8Array(hash));
      }
    
      base64URLEncode(buffer) {
        return btoa(String.fromCharCode(...buffer))
          .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
      }
    }
    
    // Usage
    const auth = new AuthClient({
      clientId: 'your_client_id',
      redirectUri: 'http://localhost:3000/callback',
      authorityUrl: 'http://localhost:4000'
    });
    
    if (window.location.search.includes('code=')) {
      auth.handleCallback().then(user => {
        console.log('Logged in as:', user.name);
      });
    }

    Local validation

    Low latency required, stateless validation

    Token introspection

    Need real-time revocation checks, opaque tokens

    read

    Read-only access

    write

    Create and update resources

    admin

    Administrative operations

    Add User Authentication
    Token Introspection Reference
    Refresh Tokens
    Environment Variables
    SSL Certificates
    Redis Caching

    verify-full - Verify server certificate and hostname

    scopes

    OAuth scopes

    audit_logs

    Audit trail

    sessions

    User sessions

    users

    User accounts

    clients

    OAuth clients

    access_tokens

    Access tokens

    refresh_tokens

    Refresh tokens

    authorization_codes

    Authorization codes

    device_codes

    Device authorization codes

    Environment Variables
    Redis Caching
    Docker Installation

    kubectl configured

  • Helm 3 (optional)

  • hashtag
    Quick Start with Manifests

    hashtag
    1. Create Namespace

    hashtag
    2. Create Secrets

    hashtag
    3. Deploy PostgreSQL

    hashtag
    4. Deploy Authority

    hashtag
    5. Configure Ingress

    hashtag
    6. Apply All Manifests

    hashtag
    Helm Chart

    hashtag
    Add Repository

    hashtag
    Install

    hashtag
    Custom Values

    Create values.yaml:

    Install with values:

    hashtag
    High Availability

    hashtag
    Database

    Use a managed PostgreSQL service (RDS, Cloud SQL) or deploy a PostgreSQL cluster:

    hashtag
    Redis for Sessions

    hashtag
    Pod Disruption Budget

    hashtag
    Monitoring

    hashtag
    ServiceMonitor (Prometheus)

    hashtag
    Logs

    View logs:

    hashtag
    Troubleshooting

    hashtag
    Check Pod Status

    hashtag
    Database Connection Issues

    hashtag
    View Events

    hashtag
    Next Steps

    • Environment Variables - Configuration options

    • SSL Certificates - TLS configuration

    • Enable MFA - Multi-factor authentication

    Environment: development, production, test

    PORT

    4000

    HTTP server port

    HOST

    0.0.0.0

    Bind address

    BASE_URL

    http://localhost:4000

    Public URL for redirects and links

    CRYSTAL_WORKERS

    4

    Number of worker processes

    hashtag
    Database

    Variable
    Default
    Description

    DATABASE_URL

    Required

    PostgreSQL connection string

    Example connection strings:

    hashtag
    Security

    Variable
    Default
    Description

    SECRET_KEY

    Required

    JWT signing key (256-bit minimum)

    Generate a secure key:

    hashtag
    Token Lifetimes

    Variable
    Default
    Description

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    CODE_TTL

    600

    Authorization code lifetime (seconds)

    DEVICE_CODE_TTL

    300

    Device code lifetime (seconds)

    hashtag
    Session

    Variable
    Default
    Description

    SESSION_KEY

    session_id

    Session cookie name

    SESSION_DURATION_DAYS

    7

    Session lifetime (days)

    hashtag
    SSL/TLS

    Variable
    Default
    Description

    SSL_CERT

    Path to SSL certificate

    SSL_KEY

    Path to SSL private key

    SSL_CA

    Path to CA certificate

    SSL_MODE

    SSL mode: require, verify-ca, verify-full

    hashtag
    Templates

    Variable
    Default
    Description

    TEMPLATES_PATH

    ./public/templates

    Path to Jinja templates

    hashtag
    Logging

    Variable
    Default
    Description

    CRYSTAL_LOG_LEVEL

    debug

    Log level: debug, info, warn, error

    CRYSTAL_LOG_SOURCES

    *

    Log sources filter

    hashtag
    Example Configuration

    hashtag
    Development

    hashtag
    Production

    hashtag
    Loading Environment Variables

    hashtag
    From File

    Authority automatically loads .env.local in development:

    hashtag
    From Shell

    hashtag
    Docker

    Or with env file:

    hashtag
    Next Steps

    • Database Setup - Database configuration

    • Redis Caching - Session storage

    • SSL Certificates - HTTPS setup

    CRYSTAL_ENV

    development

    PostgreSQL 13+
  • Git

  • hashtag
    Step 1: Clone Repository

    hashtag
    Step 2: Install Dependencies

    hashtag
    Step 3: Set Up Database

    Create the database:

    Or using psql:

    hashtag
    Step 4: Configure Environment

    Copy the example environment file:

    Edit .env.local:

    hashtag
    Step 5: Run Migrations

    hashtag
    Step 6: Seed Default Data (Optional)

    Create an admin user:

    hashtag
    Step 7: Start the Server

    Development mode:

    Or with hot reload:

    hashtag
    Production Build

    hashtag
    Compile Release Binary

    hashtag
    Run Production Server

    hashtag
    Running Tests

    Run specific tests:

    hashtag
    Development Workflow

    hashtag
    Watch for Changes

    Use watchexec or similar:

    hashtag
    Database Reset

    hashtag
    File Structure

    hashtag
    Common Issues

    hashtag
    Crystal not found

    Ensure Crystal is in your PATH:

    hashtag
    PostgreSQL connection refused

    Start PostgreSQL:

    hashtag
    Shards install fails

    Update shards cache:

    hashtag
    Port already in use

    Check what's using the port:

    hashtag
    Next Steps

    • Docker Installation - Containerized deployment

    • Database Setup - Database configuration

    • Environment Variables - All settings

    Crystalarrow-up-right

    Direct SSL - Authority handles SSL directly

    hashtag
    Option 1: Reverse Proxy (Recommended)

    hashtag
    Nginx with Let's Encrypt

    Install Certbot:

    Get certificate:

    Nginx configuration:

    Update Authority configuration:

    hashtag
    Traefik with Let's Encrypt

    Docker labels:

    hashtag
    Option 2: Direct SSL

    Authority can handle SSL directly using environment variables.

    hashtag
    Generate Self-Signed Certificate (Development)

    hashtag
    Configure Authority

    hashtag
    Using Let's Encrypt Certificates

    hashtag
    Certificate Renewal

    hashtag
    Certbot Auto-Renewal

    Certbot sets up automatic renewal. Test with:

    hashtag
    Manual Renewal Hook

    Create renewal hook to reload Authority:

    hashtag
    Security Headers

    Add security headers in your reverse proxy:

    hashtag
    Verify SSL Configuration

    Test your SSL setup:

    hashtag
    Troubleshooting

    hashtag
    Certificate chain incomplete

    Include the full chain:

    hashtag
    Permission denied

    Ensure Authority can read certificates:

    hashtag
    Mixed content warnings

    Ensure BASE_URL uses https://:

    hashtag
    Next Steps

    • Environment Variables - All configuration options

    • Docker Installation - Production setup

    • Enable MFA - Additional security

    curl http://localhost:4000/.well-known/jwks.json
    {
      "keys": [
        {
          "kty": "RSA",
          "kid": "authority-key-1",
          "use": "sig",
          "alg": "RS256",
          "n": "...",
          "e": "AQAB"
        }
      ]
    }
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'http://localhost:4000/.well-known/jwks.json',
      cache: true,
      rateLimit: true
    });
    
    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        callback(err, key?.getPublicKey());
      });
    }
    
    async function validateToken(token) {
      return new Promise((resolve, reject) => {
        jwt.verify(token, getKey, {
          algorithms: ['RS256'],
          issuer: 'http://localhost:4000'
        }, (err, decoded) => {
          if (err) reject(err);
          else resolve(decoded);
        });
      });
    }
    import jwt
    import requests
    from jwt import PyJWKClient
    
    jwks_client = PyJWKClient("http://localhost:4000/.well-known/jwks.json")
    
    def validate_token(token):
        signing_key = jwks_client.get_signing_key_from_jwt(token)
        return jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            issuer="http://localhost:4000"
        )
    async function authMiddleware(req, res, next) {
      const authHeader = req.headers.authorization;
    
      if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ error: 'Missing token' });
      }
    
      const token = authHeader.substring(7);
    
      try {
        const decoded = await validateToken(token);
        req.user = decoded;
        next();
      } catch (err) {
        return res.status(401).json({ error: 'Invalid token' });
      }
    }
    
    // Use it
    app.get('/api/protected', authMiddleware, (req, res) => {
      res.json({ message: `Hello, ${req.user.sub}` });
    });
    const CLIENT_ID = 'your_client_id';
    const CLIENT_SECRET = 'your_client_secret';
    async function introspectToken(token) {
      const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
    
      const response = await fetch('http://localhost:4000/token/introspect', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Authorization': `Basic ${credentials}`
        },
        body: new URLSearchParams({
          token: token,
          token_type_hint: 'access_token'
        })
      });
    
      return response.json();
    }
    {
      "active": true,
      "client_id": "my-client",
      "username": "user@example.com",
      "scope": "read write",
      "sub": "user-uuid",
      "exp": 1699999999
    }
    {
      "active": false
    }
    function requireScopes(...requiredScopes) {
      return async (req, res, next) => {
        const token = req.headers.authorization?.substring(7);
    
        if (!token) {
          return res.status(401).json({ error: 'Missing token' });
        }
    
        try {
          const decoded = await validateToken(token);
          const tokenScopes = decoded.scope?.split(' ') || [];
    
          const hasAllScopes = requiredScopes.every(
            scope => tokenScopes.includes(scope)
          );
    
          if (!hasAllScopes) {
            return res.status(403).json({
              error: 'Insufficient scope',
              required: requiredScopes,
              provided: tokenScopes
            });
          }
    
          req.user = decoded;
          next();
        } catch (err) {
          return res.status(401).json({ error: 'Invalid token' });
        }
      };
    }
    
    // Use it
    app.get('/api/data', requireScopes('read'), (req, res) => {
      res.json({ data: '...' });
    });
    
    app.post('/api/data', requireScopes('write'), (req, res) => {
      res.json({ created: true });
    });
    
    app.delete('/api/users/:id', requireScopes('admin'), (req, res) => {
      res.json({ deleted: true });
    });
    function scheduleTokenRefresh(tokens) {
      const expiresIn = tokens.expires_in * 1000; // Convert to ms
      const refreshAt = expiresIn - 60000; // Refresh 1 minute early
    
      setTimeout(async () => {
        const newTokens = await refreshAccessToken(tokens.refresh_token);
        scheduleTokenRefresh(newTokens);
      }, refreshAt);
    }
    app.use((err, req, res, next) => {
      if (err.name === 'TokenExpiredError') {
        return res.status(401).json({
          error: 'token_expired',
          message: 'Access token has expired'
        });
      }
      next(err);
    });
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    docker-compose up -d
    # Server
    CRYSTAL_ENV=production
    PORT=4000
    BASE_URL=https://auth.example.com
    
    # Database
    DATABASE_URL=postgres://auth_user:secure_password@db:5432/authority_db
    
    # Security
    SECRET_KEY=your-256-bit-secret-key-here
    
    # Token Lifetimes (seconds)
    ACCESS_TOKEN_TTL=3600
    CODE_TTL=600
    DEVICE_CODE_TTL=300
    
    # SSL (optional - use reverse proxy recommended)
    SSL_CERT=
    SSL_KEY=
    openssl rand -hex 32
    version: '3.8'
    
    services:
      authority:
        image: azutoolkit/authority:latest
        ports:
          - "4000:4000"
        environment:
          - CRYSTAL_ENV=production
          - DATABASE_URL=postgres://auth_user:${DB_PASSWORD}@db:5432/authority_db
          - SECRET_KEY=${SECRET_KEY}
          - BASE_URL=${BASE_URL}
        depends_on:
          db:
            condition: service_healthy
        restart: unless-stopped
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
          interval: 30s
          timeout: 10s
          retries: 3
    
      db:
        image: postgres:15-alpine
        environment:
          - POSTGRES_USER=auth_user
          - POSTGRES_PASSWORD=${DB_PASSWORD}
          - POSTGRES_DB=authority_db
        volumes:
          - postgres_data:/var/lib/postgresql/data
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U auth_user -d authority_db"]
          interval: 10s
          timeout: 5s
          retries: 5
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        volumes:
          - redis_data:/data
        restart: unless-stopped
    
    volumes:
      postgres_data:
      redis_data:
    docker-compose up -d
    server {
        listen 443 ssl http2;
        server_name auth.example.com;
    
        ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
    
        location / {
            proxy_pass http://localhost:4000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
    server {
        listen 80;
        server_name auth.example.com;
        return 301 https://$server_name$request_uri;
    }
    # docker-compose.yml addition
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authority.rule=Host(`auth.example.com`)"
      - "traefik.http.routers.authority.tls=true"
      - "traefik.http.routers.authority.tls.certresolver=letsencrypt"
    docker-compose exec authority crystal run src/db/migrate.cr
    docker-compose exec db pg_dump -U auth_user authority_db > backup.sql
    docker-compose exec -T db psql -U auth_user authority_db < backup.sql
    curl http://localhost:4000/health
    docker-compose logs -f authority
    docker stats authority
    services:
      authority:
        deploy:
          replicas: 3
    docker-compose logs authority
    docker-compose ps db
    docker-compose logs db
    PORT=4001 docker-compose up -d
    # Create database
    psql -c "CREATE DATABASE authority_db;"
    
    # Create user (optional)
    psql -c "CREATE USER authority WITH PASSWORD 'secure_password';"
    psql -c "GRANT ALL PRIVILEGES ON DATABASE authority_db TO authority;"
    createdb authority_db
    # Basic format
    DATABASE_URL=postgres://user:password@host:port/database
    
    # Examples
    DATABASE_URL=postgres://localhost:5432/authority_db
    DATABASE_URL=postgres://authority:password@localhost:5432/authority_db
    DATABASE_URL=postgres://user:password@host:5432/authority_db?sslmode=require
    crystal run src/db/migrate.cr
    crystal run src/db/seed.cr
    pg_dump -U authority authority_db > backup.sql
    pg_dump -U authority authority_db | gzip > backup.sql.gz
    psql -U authority authority_db < backup.sql
    gunzip -c backup.sql.gz | psql -U authority authority_db
    DATABASE_URL=postgres://user:password@host:5432/authority_db?initial_pool_size=10&max_pool_size=50
    # pgbouncer.ini
    [databases]
    authority_db = host=localhost port=5432 dbname=authority_db
    
    [pgbouncer]
    listen_addr = 127.0.0.1
    listen_port = 6432
    pool_mode = transaction
    max_client_conn = 200
    default_pool_size = 20
    DATABASE_URL=postgres://user:password@localhost:6432/authority_db
    # postgresql.conf
    
    # Memory
    shared_buffers = 256MB
    effective_cache_size = 768MB
    work_mem = 16MB
    
    # Connections
    max_connections = 100
    
    # Write-ahead log
    wal_buffers = 16MB
    checkpoint_completion_target = 0.9
    -- Index on frequently queried columns
    CREATE INDEX idx_tokens_user_id ON access_tokens(user_id);
    CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
    pg_isready -h localhost -p 5432
    # pg_hba.conf
    local   all   all                 trust
    host    all   all   127.0.0.1/32  md5
    createdb authority_db
    GRANT ALL PRIVILEGES ON DATABASE authority_db TO authority;
    GRANT ALL ON ALL TABLES IN SCHEMA public TO authority;
    GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO authority;
    # namespace.yaml
    apiVersion: v1
    kind: Namespace
    metadata:
      name: authority
    kubectl apply -f namespace.yaml
    # Generate secret key
    kubectl create secret generic authority-secrets \
      --namespace authority \
      --from-literal=secret-key=$(openssl rand -hex 32) \
      --from-literal=db-password=$(openssl rand -hex 16)
    # postgres.yaml
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: postgres
      namespace: authority
    spec:
      serviceName: postgres
      replicas: 1
      selector:
        matchLabels:
          app: postgres
      template:
        metadata:
          labels:
            app: postgres
        spec:
          containers:
          - name: postgres
            image: postgres:15-alpine
            ports:
            - containerPort: 5432
            env:
            - name: POSTGRES_USER
              value: authority
            - name: POSTGRES_DB
              value: authority_db
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: authority-secrets
                  key: db-password
            volumeMounts:
            - name: postgres-data
              mountPath: /var/lib/postgresql/data
            livenessProbe:
              exec:
                command: ["pg_isready", "-U", "authority"]
              initialDelaySeconds: 30
              periodSeconds: 10
      volumeClaimTemplates:
      - metadata:
          name: postgres-data
        spec:
          accessModes: ["ReadWriteOnce"]
          resources:
            requests:
              storage: 10Gi
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: postgres
      namespace: authority
    spec:
      ports:
      - port: 5432
      selector:
        app: postgres
      clusterIP: None
    # authority.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: authority
      namespace: authority
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: authority
      template:
        metadata:
          labels:
            app: authority
        spec:
          containers:
          - name: authority
            image: azutoolkit/authority:latest
            ports:
            - containerPort: 4000
            env:
            - name: CRYSTAL_ENV
              value: production
            - name: PORT
              value: "4000"
            - name: BASE_URL
              value: "https://auth.example.com"
            - name: SECRET_KEY
              valueFrom:
                secretKeyRef:
                  name: authority-secrets
                  key: secret-key
            - name: DATABASE_URL
              value: "postgres://authority:$(DB_PASSWORD)@postgres:5432/authority_db"
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: authority-secrets
                  key: db-password
            livenessProbe:
              httpGet:
                path: /health
                port: 4000
              initialDelaySeconds: 10
              periodSeconds: 30
            readinessProbe:
              httpGet:
                path: /health
                port: 4000
              initialDelaySeconds: 5
              periodSeconds: 10
            resources:
              requests:
                memory: "256Mi"
                cpu: "100m"
              limits:
                memory: "512Mi"
                cpu: "500m"
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: authority
      namespace: authority
    spec:
      ports:
      - port: 80
        targetPort: 4000
      selector:
        app: authority
    # ingress.yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: authority
      namespace: authority
      annotations:
        kubernetes.io/ingress.class: nginx
        cert-manager.io/cluster-issuer: letsencrypt-prod
    spec:
      tls:
      - hosts:
        - auth.example.com
        secretName: authority-tls
      rules:
      - host: auth.example.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: authority
                port:
                  number: 80
    kubectl apply -f namespace.yaml
    kubectl apply -f postgres.yaml
    kubectl apply -f authority.yaml
    kubectl apply -f ingress.yaml
    helm repo add authority https://azutoolkit.github.io/authority-helm
    helm repo update
    helm install authority authority/authority \
      --namespace authority \
      --create-namespace \
      --set ingress.enabled=true \
      --set ingress.hosts[0].host=auth.example.com \
      --set postgresql.enabled=true
    replicaCount: 3
    
    image:
      repository: azutoolkit/authority
      tag: latest
    
    ingress:
      enabled: true
      className: nginx
      annotations:
        cert-manager.io/cluster-issuer: letsencrypt-prod
      hosts:
        - host: auth.example.com
          paths:
            - path: /
              pathType: Prefix
      tls:
        - secretName: authority-tls
          hosts:
            - auth.example.com
    
    postgresql:
      enabled: true
      auth:
        database: authority_db
        username: authority
      primary:
        persistence:
          size: 10Gi
    
    redis:
      enabled: true
    
    resources:
      requests:
        memory: 256Mi
        cpu: 100m
      limits:
        memory: 512Mi
        cpu: 500m
    
    autoscaling:
      enabled: true
      minReplicas: 3
      maxReplicas: 10
      targetCPUUtilizationPercentage: 80
    helm install authority authority/authority \
      --namespace authority \
      --create-namespace \
      -f values.yaml
    postgresql:
      enabled: false
    
    externalDatabase:
      host: your-postgres.xxx.rds.amazonaws.com
      port: 5432
      database: authority_db
      username: authority
      existingSecret: authority-db-secret
    redis:
      enabled: true
      architecture: replication
      replica:
        replicaCount: 2
    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: authority-pdb
      namespace: authority
    spec:
      minAvailable: 2
      selector:
        matchLabels:
          app: authority
    apiVersion: monitoring.coreos.com/v1
    kind: ServiceMonitor
    metadata:
      name: authority
      namespace: authority
    spec:
      selector:
        matchLabels:
          app: authority
      endpoints:
      - port: http
        path: /metrics
    kubectl logs -f -l app=authority -n authority
    kubectl get pods -n authority
    kubectl describe pod authority-xxx -n authority
    kubectl exec -it authority-xxx -n authority -- sh
    curl postgres:5432
    kubectl get events -n authority --sort-by='.lastTimestamp'
    # Local development
    DATABASE_URL=postgres://localhost:5432/authority_db
    
    # With credentials
    DATABASE_URL=postgres://user:password@localhost:5432/authority_db
    
    # With SSL
    DATABASE_URL=postgres://user:password@host:5432/authority_db?sslmode=require
    openssl rand -hex 32
    # .env.local
    CRYSTAL_ENV=development
    PORT=4000
    BASE_URL=http://localhost:4000
    DATABASE_URL=postgres://localhost:5432/authority_db
    SECRET_KEY=development-key-not-for-production
    CRYSTAL_LOG_LEVEL=debug
    # .env
    CRYSTAL_ENV=production
    PORT=4000
    HOST=0.0.0.0
    BASE_URL=https://auth.example.com
    DATABASE_URL=postgres://user:password@db.example.com:5432/authority_db?sslmode=require
    SECRET_KEY=your-256-bit-production-secret-key
    ACCESS_TOKEN_TTL=3600
    SESSION_DURATION_DAYS=7
    CRYSTAL_LOG_LEVEL=info
    CRYSTAL_WORKERS=8
    crystal run src/app.cr
    export DATABASE_URL=postgres://localhost:5432/authority_db
    crystal run src/app.cr
    docker run -e DATABASE_URL=... -e SECRET_KEY=... azutoolkit/authority
    docker run --env-file .env azutoolkit/authority
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    shards install
    createdb authority_db
    psql -c "CREATE DATABASE authority_db;"
    cp .env.example .env.local
    # Server
    CRYSTAL_ENV=development
    PORT=4000
    BASE_URL=http://localhost:4000
    
    # Database
    DATABASE_URL=postgres://localhost:5432/authority_db
    
    # Security
    SECRET_KEY=development-secret-key-change-in-production
    
    # Token Lifetimes
    ACCESS_TOKEN_TTL=3600
    CODE_TTL=600
    crystal run src/db/migrate.cr
    crystal run src/db/seed.cr
    crystal run src/app.cr
    ./scripts/dev.sh
    crystal build src/app.cr --release -o bin/authority
    CRYSTAL_ENV=production ./bin/authority
    crystal spec
    crystal spec spec/models/user_spec.cr
    watchexec -e cr -r "crystal run src/app.cr"
    dropdb authority_db
    createdb authority_db
    crystal run src/db/migrate.cr
    crystal run src/db/seed.cr
    authority/
    ├── src/
    │   ├── app.cr              # Application entry point
    │   ├── db/
    │   │   ├── migrate.cr      # Migration runner
    │   │   ├── seed.cr         # Seed data
    │   │   └── migrations/     # Migration files
    │   ├── endpoints/          # HTTP handlers
    │   ├── models/             # Data models
    │   ├── services/           # Business logic
    │   └── views/              # Response serializers
    ├── public/
    │   ├── templates/          # Jinja templates
    │   ├── css/                # Stylesheets
    │   └── js/                 # JavaScript
    ├── spec/                   # Tests
    ├── shard.yml               # Dependencies
    └── .env.example            # Environment template
    export PATH="$PATH:/usr/local/crystal/bin"
    # macOS
    brew services start postgresql
    
    # Linux
    sudo systemctl start postgresql
    rm -rf lib .shards
    shards install
    lsof -i :4000
    sudo apt install certbot python3-certbot-nginx
    sudo certbot --nginx -d auth.example.com
    server {
        listen 443 ssl http2;
        server_name auth.example.com;
    
        ssl_certificate /etc/letsencrypt/live/auth.example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/auth.example.com/privkey.pem;
    
        # SSL settings
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
        ssl_prefer_server_ciphers off;
    
        # HSTS
        add_header Strict-Transport-Security "max-age=63072000" always;
    
        location / {
            proxy_pass http://127.0.0.1:4000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    
    server {
        listen 80;
        server_name auth.example.com;
        return 301 https://$server_name$request_uri;
    }
    BASE_URL=https://auth.example.com
    # traefik.yml
    entryPoints:
      web:
        address: ":80"
        http:
          redirections:
            entryPoint:
              to: websecure
              scheme: https
      websecure:
        address: ":443"
    
    certificatesResolvers:
      letsencrypt:
        acme:
          email: admin@example.com
          storage: /letsencrypt/acme.json
          httpChallenge:
            entryPoint: web
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.authority.rule=Host(`auth.example.com`)"
      - "traefik.http.routers.authority.entrypoints=websecure"
      - "traefik.http.routers.authority.tls.certresolver=letsencrypt"
    openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
      -keyout authority.key \
      -out authority.crt \
      -subj "/CN=localhost"
    SSL_CERT=/path/to/authority.crt
    SSL_KEY=/path/to/authority.key
    BASE_URL=https://localhost:4000
    SSL_CERT=/etc/letsencrypt/live/auth.example.com/fullchain.pem
    SSL_KEY=/etc/letsencrypt/live/auth.example.com/privkey.pem
    BASE_URL=https://auth.example.com
    sudo certbot renew --dry-run
    # /etc/letsencrypt/renewal-hooks/post/reload-authority.sh
    #!/bin/bash
    docker-compose -f /path/to/docker-compose.yml restart authority
    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always;
    # Check certificate
    openssl s_client -connect auth.example.com:443 -servername auth.example.com
    
    # SSL Labs test
    # Visit: https://www.ssllabs.com/ssltest/analyze.html?d=auth.example.com
    SSL_CERT=/path/to/fullchain.pem  # Not just cert.pem
    chmod 644 authority.crt
    chmod 600 authority.key
    BASE_URL=https://auth.example.com
    Developer contact: Your email

    Compliance requirements

  • Incident investigation

  • User activity tracking

  • hashtag
    Logged Events

    Event
    Description

    user.login

    Successful login

    user.login_failed

    Failed login attempt

    user.logout

    User logout

    user.created

    New user registration

    user.updated

    Profile update

    user.deleted

    User deletion

    hashtag
    Log Entry Format

    Each log entry includes:

    Field
    Description

    id

    Unique log entry ID

    timestamp

    When the event occurred

    event

    Event type

    actor_id

    User or client who performed action

    actor_type

    user, client, or system

    resource_type

    Type of resource affected

    Example entry:

    hashtag
    Viewing Logs

    hashtag
    Admin Dashboard

    1. Navigate to Admin Dashboard → Audit Logs

    2. Use filters to find specific events

    Audit Logs

    hashtag
    Filter Options

    • Date range - Start and end dates

    • Event type - Filter by specific event

    • User - Filter by actor

    • Resource - Filter by affected resource

    • IP address - Filter by source IP

    hashtag
    Export

    Export logs in various formats:

    • CSV - For spreadsheet analysis

    • JSON - For processing with scripts

    • PDF - For reports

    hashtag
    API Access

    hashtag
    List Logs

    Response:

    hashtag
    Filter Logs

    hashtag
    Get Single Entry

    hashtag
    Log Retention

    hashtag
    Configuration

    hashtag
    Manual Cleanup

    hashtag
    External Log Shipping

    hashtag
    Syslog

    hashtag
    File Output

    hashtag
    SIEM Integration

    Export logs to security information systems:

    hashtag
    Monitoring and Alerting

    hashtag
    Failed Login Alerts

    hashtag
    Suspicious Activity

    Monitor for:

    • Multiple failed logins from same IP

    • Admin account logins from new IPs

    • MFA disabled events

    • Client secret rotations

    hashtag
    Compliance

    hashtag
    GDPR

    Audit logs help demonstrate:

    • Who accessed user data

    • What changes were made

    • When access occurred

    hashtag
    SOC 2

    Logs provide evidence of:

    • Access controls

    • Monitoring activities

    • Incident response

    hashtag
    Best Practices

    circle-info
    • Enable log shipping to external systems

    • Retain logs for at least 90 days

    • Set up alerts for critical events

    • Regularly review logs for anomalies

    circle-exclamation
    • Don't log sensitive data (passwords, tokens)

    • Protect log access with strong controls

    • Consider log integrity (tampering detection)

    hashtag
    Next Steps

    • Enable MFA - Multi-factor authentication

    • Account Lockout - Brute-force protection

    • Password Policies - Password requirements

    Variable
    Default
    Description

    PASSWORD_MIN_LENGTH

    12

    Minimum password length

    PASSWORD_HISTORY_COUNT

    5

    Prevent reuse of recent passwords

    PASSWORD_EXPIRY_DAYS

    0

    Days until password expires (0 = never)

    REQUIRE_UPPERCASE

    true

    Require uppercase letters

    hashtag
    Example Configurations

    hashtag
    Standard Security

    hashtag
    High Security

    hashtag
    User-Friendly

    hashtag
    Password Validation

    When a user sets a password, Authority validates:

    1. Length - Meets minimum requirement

    2. Complexity - Contains required character types

    3. History - Not recently used

    4. Strength - Not in common password lists (optional)

    hashtag
    Validation Messages

    Users see clear feedback:

    hashtag
    Password Expiry

    When PASSWORD_EXPIRY_DAYS is set, users must change passwords periodically.

    hashtag
    Expiry Warning

    Users are warned before expiry:

    hashtag
    Grace Period

    Allow logins during grace period:

    hashtag
    Handling Expired Passwords

    When a password expires:

    1. User logs in with expired password

    2. Authority forces password change

    3. User cannot access application until password is changed

    hashtag
    Password History

    Prevent password reuse:

    If a user tries to reuse a password:

    hashtag
    Common Password Check

    Block commonly used passwords:

    Download a password list:

    hashtag
    API Integration

    hashtag
    Validate Password

    Response:

    hashtag
    Check Password Expiry

    Response:

    hashtag
    User Experience

    hashtag
    Password Strength Indicator

    The UI shows password strength in real-time:

    • Weak - Red, doesn't meet requirements

    • Fair - Yellow, meets minimum

    • Strong - Green, exceeds requirements

    hashtag
    Password Generator

    Offer a password generator:

    hashtag
    Best Practices

    circle-check

    Modern recommendations (NIST SP 800-63B):

    • Focus on length over complexity

    • Allow spaces and all printable characters

    • Check against breach databases

    • Avoid forced periodic rotation

    circle-exclamation

    Avoid:

    • Very short minimum lengths (< 8)

    • Forced rotation without reason

    • Overly complex rules that encourage weak patterns

    hashtag
    Troubleshooting

    hashtag
    Password rejected unexpectedly

    • Check which rules are enabled

    • Verify character encoding

    • Review password history

    hashtag
    Users forgetting complex passwords

    • Consider reducing complexity requirements

    • Enable password manager hints

    • Implement "forgot password" flow

    hashtag
    Next Steps

    • Enable MFA - Multi-factor authentication

    • Account Lockout - Brute-force protection

    • Audit Logging - Track security events

    Protect Your API
    Authorization Code Reference
    PKCE Reference

    Security

    Set up TOTP-based two-factor authentication for Authority users.

    hashtag
    Overview

    Authority supports Time-based One-Time Passwords (TOTP) for MFA. Users can use apps like:

    • Google Authenticator

    • Authy

    • 1Password

    • Microsoft Authenticator

    hashtag
    MFA Flow

    hashtag
    User Setup

    hashtag
    Step 1: Navigate to Profile

    1. Log in to Authority

    2. Click your profile name

    3. Select Security Settings

    hashtag
    Step 2: Enable MFA

    1. Click Enable Two-Factor Authentication

    2. Authority displays a QR code

    hashtag
    Step 3: Scan QR Code

    1. Open your authenticator app

    2. Tap Add Account or +

    3. Scan the QR code

    hashtag
    Step 4: Verify Setup

    1. Enter the 6-digit code from your app

    2. Click Verify

    3. Save your backup codes in a secure location

    hashtag
    Backup Codes

    When MFA is enabled, Authority generates 10 one-time backup codes. Store these securely - they can be used if you lose access to your authenticator app.

    Example backup codes:

    Each code can only be used once.

    hashtag
    Admin Configuration

    hashtag
    Require MFA for Admins

    Enforce MFA for all administrator accounts:

    hashtag
    Require MFA for All Users

    Force MFA enrollment for all users:

    hashtag
    MFA Grace Period

    Allow users time to set up MFA:

    hashtag
    Disabling MFA

    hashtag
    User Self-Service

    Users can disable MFA from their profile if allowed:

    1. Navigate to Security Settings

    2. Click Disable Two-Factor Authentication

    3. Enter current TOTP code to confirm

    hashtag
    Admin Override

    Administrators can disable MFA for users:

    1. Go to Admin Dashboard → Users

    2. Select the user

    3. Click Disable MFA

    This is useful when a user loses access to their authenticator.

    hashtag
    API Integration

    hashtag
    Check MFA Status

    Response:

    hashtag
    Require MFA in Authorization

    During OAuth authorization, check if MFA is required:

    hashtag
    Recovery

    hashtag
    Lost Authenticator

    If a user loses their phone:

    1. Use a backup code to log in

    2. Disable MFA

    3. Set up MFA again with new device

    hashtag
    Lost Backup Codes

    If a user loses both authenticator and backup codes:

    1. Admin disables MFA for the user

    2. User logs in with password only

    3. User sets up MFA again

    4. User saves new backup codes

    hashtag
    Security Considerations

    circle-exclamation
    • Backup codes should be stored securely (password manager, safe)

    • Regenerate backup codes periodically

    hashtag
    Troubleshooting

    hashtag
    Invalid TOTP Code

    • Verify device time is synchronized

    • Ensure you're using the correct account in the authenticator

    • Wait for the next code cycle (30 seconds)

    hashtag
    QR Code Won't Scan

    • Use the manual entry option

    • Enter the secret key directly into your authenticator

    hashtag
    Locked Out

    • Use a backup code

    • Contact administrator to disable MFA

    hashtag
    Next Steps

    • - Brute-force protection

    • - Password requirements

    • - Track security events

    Social Login

    Configure social authentication providers to allow users to sign in with their existing accounts from Google, GitHub, Facebook, LinkedIn, and Apple.

    hashtag
    Available Guides

    • Configure Google - Enable Google Sign-In

    • - Enable GitHub authentication

    • - Enable Facebook Login

    • - Enable LinkedIn Sign-In

    • - Enable Sign in with Apple

    • - Link/unlink social accounts

    hashtag
    Overview

    Social login (also called social sign-in or OAuth federation) allows users to authenticate using their existing accounts from popular identity providers. This provides:

    • Reduced friction - Users don't need to create new passwords

    • Improved security - Leverage provider's security features

    • Verified emails - Many providers verify user emails

    hashtag
    How It Works

    hashtag
    Supported Providers

    Provider
    Scopes
    Features

    hashtag
    Quick Start

    1. Create OAuth app with your chosen provider

    2. Configure credentials in Authority admin dashboard

    3. Enable provider in settings

    See individual provider guides for detailed setup instructions.

    hashtag
    Security Considerations

    • Always use HTTPS in production

    • Store client secrets securely

    • Validate state parameter to prevent CSRF

    hashtag
    Next Steps

    • - Most common provider

    • - Account linking features

    Redis Caching

    Configure Redis for session storage and caching.

    hashtag
    Why Use Redis?

    • Session storage - Share sessions across multiple Authority instances

    • Token caching - Faster token validation

    • Rate limiting - Distributed rate limit counters

    hashtag
    Prerequisites

    • Redis 6.0+

    • Network access between Authority and Redis

    hashtag
    Basic Setup

    hashtag
    Install Redis

    Docker:

    macOS:

    Ubuntu:

    hashtag
    Configure Authority

    Set the Redis URL:

    With password:

    With database number:

    hashtag
    Production Configuration

    hashtag
    Redis Configuration

    hashtag
    TLS/SSL

    hashtag
    Docker Compose

    hashtag
    High Availability

    hashtag
    Redis Sentinel

    hashtag
    Redis Cluster

    hashtag
    Session Storage

    With Redis enabled, sessions are stored in Redis instead of PostgreSQL:

    Session expiry is handled automatically by Redis TTL.

    hashtag
    Token Caching

    Access tokens are cached for faster validation:

    hashtag
    Rate Limiting

    Rate limit counters use Redis:

    hashtag
    Monitoring

    hashtag
    Redis CLI

    hashtag
    Check Connection

    hashtag
    Troubleshooting

    hashtag
    Connection refused

    Check Redis is running:

    hashtag
    Authentication failed

    Verify password:

    hashtag
    Memory full

    Check memory usage:

    Increase maxmemory or use eviction policy.

    hashtag
    Next Steps

    • - All configuration options

    • - Containerized setup

    • - Scalable deployment

    Configure Google

    Enable users to sign in with their Google accounts.

    hashtag
    Prerequisites

    • Authority instance running

    • Admin access to Authority dashboard

    • Google Cloud Console account

    hashtag
    Step 1: Create Google OAuth App

    1. Go to

    2. Create a new project or select existing one

    3. Navigate to APIs & Services > Credentials

    hashtag
    Step 2: Configure Authority

    hashtag
    Using Admin Dashboard

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable Google OAuth

    hashtag
    Using Environment Variables

    hashtag
    Step 3: Add Login Button

    Add a Google sign-in button to your application:

    With forward URL (redirect after login):

    hashtag
    Step 4: Test the Integration

    1. Click your Google sign-in button

    2. You should be redirected to Google's consent page

    3. After approving, you'll be redirected back to Authority

    hashtag
    User Data Retrieved

    Authority fetches the following from Google:

    Field
    Description

    hashtag
    Troubleshooting

    hashtag
    "redirect_uri_mismatch" Error

    The callback URL doesn't match what's configured in Google Console.

    Solution: Ensure the redirect URI in Google Console exactly matches:

    hashtag
    "Access blocked: App not verified"

    Your app is in testing mode and the user isn't a test user.

    Solution: Either:

    • Add the user as a test user in Google Console

    • Submit your app for verification (production)

    hashtag
    "Invalid client" Error

    The client ID or secret is incorrect.

    Solution:

    • Verify credentials in Authority settings

    • Check for extra spaces or characters

    • Regenerate secret if needed

    hashtag
    User Not Created

    Check Authority logs for errors. Common issues:

    • Email already exists with different provider

    • Database connection issues

    • Missing required scopes

    hashtag
    Security Best Practices

    1. Verify emails - Google provides email_verified claim

    2. Use HTTPS - Required for OAuth callbacks

    3. Restrict domains - In Google Console, you can restrict to your domain

    hashtag
    Next Steps

    • - Add another provider

    • - Account linking

    • - Add extra security

    Configure GitHub

    Enable users to sign in with their GitHub accounts.

    hashtag
    Prerequisites

    • Authority instance running

    • Admin access to Authority dashboard

    • GitHub account

    hashtag
    Step 1: Create GitHub OAuth App

    1. Go to

    2. Click OAuth Apps > New OAuth App

    3. Fill in the application details:

    hashtag
    Step 2: Configure Authority

    hashtag
    Using Admin Dashboard

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable GitHub OAuth

    hashtag
    Using Environment Variables

    hashtag
    Step 3: Add Login Button

    With forward URL:

    hashtag
    Step 4: Test the Integration

    1. Click your GitHub sign-in button

    2. Authorize the OAuth app on GitHub

    3. You'll be redirected back to Authority

    4. Account created/linked automatically

    hashtag
    User Data Retrieved

    Field
    Description

    hashtag
    GitHub Organizations

    To restrict access to specific organizations, you can check membership after authentication in your application logic.

    hashtag
    Troubleshooting

    hashtag
    "Bad credentials" Error

    Client ID or secret is incorrect.

    Solution: Regenerate the client secret in GitHub and update Authority settings.

    hashtag
    No Email Retrieved

    GitHub email is private and user didn't authorize email scope.

    Solution: The user:email scope is requested by default. If email is still missing:

    • User may not have a verified email on GitHub

    • User may have denied email permission

    hashtag
    "Redirect URI mismatch"

    Callback URL doesn't match GitHub app configuration.

    Solution: Ensure exact match:

    hashtag
    GitHub Enterprise

    For GitHub Enterprise Server, contact your administrator about custom OAuth endpoints.

    hashtag
    Next Steps

    • - Add Google sign-in

    • - Link multiple providers

    Configure LinkedIn

    Enable users to sign in with their LinkedIn accounts.

    hashtag
    Prerequisites

    • Authority instance running

    • Admin access to Authority dashboard

    • LinkedIn Developer account

    hashtag
    Step 1: Create LinkedIn App

    1. Go to

    2. Click Create App

    3. Fill in app details:

    hashtag
    Step 2: Configure Authority

    hashtag
    Using Admin Dashboard

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable LinkedIn OAuth

    hashtag
    Using Environment Variables

    hashtag
    Step 3: Add Login Button

    With forward URL:

    hashtag
    User Data Retrieved

    Field
    Description

    hashtag
    Troubleshooting

    hashtag
    "Invalid redirect_uri"

    Callback URL not registered in LinkedIn app.

    Solution: Add exact URL to Authorized redirect URLs:

    hashtag
    "Unauthorized scope"

    Requested scope not approved for your app.

    Solution:

    • Ensure you've added "Sign In with LinkedIn using OpenID Connect" product

    • Wait for product access approval

    hashtag
    "Application not found"

    Client ID is incorrect or app is deleted.

    Solution: Verify Client ID in LinkedIn Developer Console.

    hashtag
    LinkedIn API Versions

    Authority uses LinkedIn's OpenID Connect implementation which provides:

    • Standard OIDC claims

    • Simplified integration

    • Better long-term stability

    hashtag
    Next Steps

    • - Add more providers

    • - Account linking

    Manage Linked Accounts

    Allow users to connect and disconnect social accounts from their profile.

    hashtag
    Overview

    Users can:

    • Link multiple social providers to one account

    User Management

    Create and manage administrator accounts in Authority.

    hashtag
    Admin Capabilities

    Administrators can:

    • Manage OAuth clients

    Account Lockout

    Protect against brute-force attacks with account lockout.

    hashtag
    How It Works

    After a configured number of failed login attempts, the account is temporarily locked:

    hashtag

    Configure Apple

    Enable users to sign in with their Apple ID.

    hashtag
    Prerequisites

    • Authority instance running

    Configure Scopes

    Create and manage OAuth scopes for access control.

    hashtag
    Overview

    Scopes define what access a client can request:

    • Standard scopes - OpenID Connect scopes (openid, profile, email)

    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "timestamp": "2024-01-15T10:30:00Z",
      "event": "user.login",
      "actor_id": "user-uuid",
      "actor_type": "user",
      "resource_type": "session",
      "resource_id": "session-uuid",
      "ip_address": "192.168.1.100",
      "user_agent": "Mozilla/5.0...",
      "metadata": {
        "method": "password",
        "mfa_used": true
      }
    }
    GET /api/audit-logs?limit=100&offset=0
    Authorization: Bearer {admin_token}
    {
      "data": [
        {
          "id": "...",
          "timestamp": "2024-01-15T10:30:00Z",
          "event": "user.login",
          ...
        }
      ],
      "total": 1250,
      "limit": 100,
      "offset": 0
    }
    GET /api/audit-logs?event=user.login_failed&from=2024-01-01&to=2024-01-31
    Authorization: Bearer {admin_token}
    GET /api/audit-logs/{id}
    Authorization: Bearer {admin_token}
    # Retain logs for 90 days
    AUDIT_LOG_RETENTION_DAYS=90
    
    # Run cleanup daily
    AUDIT_LOG_CLEANUP_SCHEDULE=0 2 * * *
    crystal run src/tasks/cleanup_audit_logs.cr
    AUDIT_LOG_SYSLOG=true
    SYSLOG_HOST=logs.example.com
    SYSLOG_PORT=514
    AUDIT_LOG_FILE=/var/log/authority/audit.log
    AUDIT_LOG_FORMAT=json
    # Splunk
    AUDIT_LOG_SPLUNK_URL=https://splunk.example.com:8088
    AUDIT_LOG_SPLUNK_TOKEN=your-hec-token
    
    # Elasticsearch
    AUDIT_LOG_ELASTICSEARCH_URL=https://elasticsearch.example.com:9200
    AUDIT_LOG_ELASTICSEARCH_INDEX=authority-audit
    # Alert after 10 failed logins in 5 minutes
    grep "user.login_failed" /var/log/authority/audit.log | \
      awk -v d="$(date -d '5 minutes ago' +%Y-%m-%dT%H:%M)" '$2 > d' | \
      wc -l | \
      xargs -I {} sh -c '[ {} -gt 10 ] && echo "Alert: High failed logins"'
    PASSWORD_MIN_LENGTH=12
    REQUIRE_UPPERCASE=true
    REQUIRE_LOWERCASE=true
    REQUIRE_NUMBERS=true
    REQUIRE_SPECIAL=false
    PASSWORD_HISTORY_COUNT=5
    PASSWORD_EXPIRY_DAYS=0
    PASSWORD_MIN_LENGTH=16
    REQUIRE_UPPERCASE=true
    REQUIRE_LOWERCASE=true
    REQUIRE_NUMBERS=true
    REQUIRE_SPECIAL=true
    PASSWORD_HISTORY_COUNT=12
    PASSWORD_EXPIRY_DAYS=90
    PASSWORD_MIN_LENGTH=10
    REQUIRE_UPPERCASE=false
    REQUIRE_LOWERCASE=false
    REQUIRE_NUMBERS=false
    REQUIRE_SPECIAL=false
    PASSWORD_HISTORY_COUNT=3
    PASSWORD_EXPIRY_DAYS=0
    Password must:
    ✗ Be at least 12 characters
    ✓ Contain an uppercase letter
    ✓ Contain a lowercase letter
    ✗ Contain a number
    PASSWORD_EXPIRY_WARNING_DAYS=14
    PASSWORD_EXPIRY_GRACE_DAYS=7
    # Remember last 5 passwords
    PASSWORD_HISTORY_COUNT=5
    This password was used recently. Please choose a different password.
    CHECK_COMMON_PASSWORDS=true
    COMMON_PASSWORD_LIST=/path/to/passwords.txt
    wget https://github.com/danielmiessler/SecLists/raw/master/Passwords/Common-Credentials/10k-most-common.txt -O passwords.txt
    POST /api/validate-password
    Content-Type: application/json
    
    {
      "password": "MyNewPassword123"
    }
    {
      "valid": false,
      "errors": [
        "Password must contain a special character"
      ]
    }
    GET /api/users/{id}/password-status
    Authorization: Bearer {token}
    {
      "last_changed": "2024-01-01T00:00:00Z",
      "expires_at": "2024-04-01T00:00:00Z",
      "days_until_expiry": 45,
      "requires_change": false
    }
    function generatePassword(length = 16) {
      const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
      let password = '';
      for (let i = 0; i < length; i++) {
        password += chars.charAt(Math.floor(Math.random() * chars.length));
      }
      return password;
    }
    // Generate random code verifier
    function generateCodeVerifier() {
      const array = new Uint8Array(32);
      crypto.getRandomValues(array);
      return base64URLEncode(array);
    }
    
    // Create code challenge from verifier
    async function generateCodeChallenge(verifier) {
      const encoder = new TextEncoder();
      const data = encoder.encode(verifier);
      const hash = await crypto.subtle.digest('SHA-256', data);
      return base64URLEncode(new Uint8Array(hash));
    }
    
    function base64URLEncode(buffer) {
      return btoa(String.fromCharCode(...buffer))
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
    }
    const CLIENT_ID = 'your_client_id';
    const REDIRECT_URI = 'http://localhost:3000/callback';
    const AUTHORITY_URL = 'http://localhost:4000';
    
    async function login() {
      const codeVerifier = generateCodeVerifier();
      const codeChallenge = await generateCodeChallenge(codeVerifier);
      const state = generateCodeVerifier(); // Random state for CSRF protection
    
      // Store for later
      sessionStorage.setItem('code_verifier', codeVerifier);
      sessionStorage.setItem('state', state);
    
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scope: 'openid profile email',
        state: state,
        code_challenge: codeChallenge,
        code_challenge_method: 'S256'
      });
    
      window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
    }
    // On your callback page
    async function handleCallback() {
      const params = new URLSearchParams(window.location.search);
      const code = params.get('code');
      const state = params.get('state');
    
      // Verify state matches
      if (state !== sessionStorage.getItem('state')) {
        throw new Error('Invalid state parameter');
      }
    
      // Get stored code verifier
      const codeVerifier = sessionStorage.getItem('code_verifier');
    
      // Exchange code for tokens
      const tokens = await exchangeCode(code, codeVerifier);
      console.log('Access token:', tokens.access_token);
    }
    async function exchangeCode(code, codeVerifier) {
      const response = await fetch(`${AUTHORITY_URL}/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          code: code,
          redirect_uri: REDIRECT_URI,
          client_id: CLIENT_ID,
          code_verifier: codeVerifier
        })
      });
    
      if (!response.ok) {
        throw new Error('Token exchange failed');
      }
    
      return response.json();
    }
    {
      "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
      "scope": "openid profile email"
    }
    async function fetchUserInfo(accessToken) {
      const response = await fetch(`${AUTHORITY_URL}/userinfo`, {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
    
      return response.json();
    }
    
    // Result:
    // {
    //   "sub": "user-uuid",
    //   "name": "John Doe",
    //   "email": "john@example.com"
    // }
    <!DOCTYPE html>
    <html>
    <head>
      <title>OAuth Demo</title>
    </head>
    <body>
      <button id="login">Login with Authority</button>
      <div id="user"></div>
    
      <script>
        const CLIENT_ID = 'your_client_id';
        const REDIRECT_URI = 'http://localhost:3000/callback';
        const AUTHORITY_URL = 'http://localhost:4000';
    
        // Check if this is a callback
        if (window.location.search.includes('code=')) {
          handleCallback();
        }
    
        document.getElementById('login').onclick = login;
    
        async function login() {
          const codeVerifier = generateCodeVerifier();
          const codeChallenge = await generateCodeChallenge(codeVerifier);
          const state = generateCodeVerifier();
    
          sessionStorage.setItem('code_verifier', codeVerifier);
          sessionStorage.setItem('state', state);
    
          const params = new URLSearchParams({
            response_type: 'code',
            client_id: CLIENT_ID,
            redirect_uri: REDIRECT_URI,
            scope: 'openid profile email',
            state: state,
            code_challenge: codeChallenge,
            code_challenge_method: 'S256'
          });
    
          window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
        }
    
        async function handleCallback() {
          const params = new URLSearchParams(window.location.search);
          const code = params.get('code');
          const state = params.get('state');
    
          if (state !== sessionStorage.getItem('state')) {
            alert('Invalid state');
            return;
          }
    
          const codeVerifier = sessionStorage.getItem('code_verifier');
    
          const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: new URLSearchParams({
              grant_type: 'authorization_code',
              code: code,
              redirect_uri: REDIRECT_URI,
              client_id: CLIENT_ID,
              code_verifier: codeVerifier
            })
          });
    
          const tokens = await tokenResponse.json();
    
          const userResponse = await fetch(`${AUTHORITY_URL}/userinfo`, {
            headers: { 'Authorization': `Bearer ${tokens.access_token}` }
          });
    
          const user = await userResponse.json();
          document.getElementById('user').textContent = `Welcome, ${user.name}!`;
        }
    
        function generateCodeVerifier() {
          const array = new Uint8Array(32);
          crypto.getRandomValues(array);
          return base64URLEncode(array);
        }
    
        async function generateCodeChallenge(verifier) {
          const data = new TextEncoder().encode(verifier);
          const hash = await crypto.subtle.digest('SHA-256', data);
          return base64URLEncode(new Uint8Array(hash));
        }
    
        function base64URLEncode(buffer) {
          return btoa(String.fromCharCode(...buffer))
            .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
        }
      </script>
    </body>
    </html>

    REQUIRE_LOWERCASE

    true

    Require lowercase letters

    REQUIRE_NUMBERS

    true

    Require numeric digits

    REQUIRE_SPECIAL

    false

    Require special characters

    user.locked

    Account locked

    user.unlocked

    Account unlocked

    mfa.enabled

    MFA enabled

    mfa.disabled

    MFA disabled

    password.changed

    Password change

    password.reset

    Password reset request

    token.issued

    Token issued

    token.revoked

    Token revocation

    client.created

    OAuth client created

    client.updated

    OAuth client updated

    client.deleted

    OAuth client deleted

    scope.created

    Scope created

    scope.updated

    Scope updated

    authorization.granted

    User granted authorization

    authorization.denied

    User denied authorization

    resource_id

    ID of affected resource

    ip_address

    Client IP address

    user_agent

    Browser/client information

    metadata

    Additional event-specific data

    Environment Variables
    Docker Installation
    Kubernetes Deployment
    Click Create Credentials > OAuth client ID
  • If prompted, configure the OAuth consent screen:

    • Choose External for public apps or Internal for organization-only

    • Fill in app name, user support email, and developer contact

    • Add scopes: email, profile, openid

    • Add test users if in testing mode

  • For OAuth client ID:

    • Application type: Web application

    • Name: Your app name

    • Authorized redirect URIs: https://your-authority-domain/auth/google/callback

  • Save your Client ID and Client Secret

  • Enter your credentials:
    • Client ID: your-google-client-id.apps.googleusercontent.com

    • Client Secret: your-google-client-secret

  • Save settings

  • A new user account is created (or existing account linked)
  • You're redirected to your application

  • picture

    Profile picture URL

    Review permissions - Only request scopes you need

  • Monitor usage - Check Google Console for suspicious activity

  • sub

    Unique Google user ID

    email

    User's email address

    email_verified

    Whether email is verified

    name

    Full name

    given_name

    First name

    family_name

    Last name

    Google Cloud Consolearrow-up-right
    Configure GitHub
    Manage Linked Accounts
    Enable MFA
    Application name: Your app name
  • Homepage URL: https://your-app.com

  • Authorization callback URL: https://your-authority-domain/auth/github/callback

  • Click Register application

  • On the app page:

    • Copy the Client ID

    • Click Generate a new client secret

    • Copy the Client Secret immediately (shown only once)

  • Enter your credentials:
    • Client ID: Your GitHub client ID

    • Client Secret: Your GitHub client secret

  • Save settings

  • Redirected to your application

  • id

    Unique GitHub user ID

    login

    GitHub username

    email

    Primary email (if public or authorized)

    name

    Display name

    avatar_url

    Profile picture URL

    GitHub Developer Settingsarrow-up-right
    Configure Google
    Manage Linked Accounts
    App name: Your application name
  • LinkedIn Page: Select or create a company page

  • Privacy policy URL: Your privacy policy

  • App logo: Upload your logo

  • Click Create app

  • In the Auth tab:

    • Note your Client ID and Client Secret

    • Add Authorized redirect URLs:

  • In the Products tab:

    • Request access to Sign In with LinkedIn using OpenID Connect

    • This provides openid, profile, and email scopes

  • Enter your credentials:
    • Client ID: Your LinkedIn Client ID

    • Client Secret: Your LinkedIn Client Secret

  • Save settings

  • picture

    Profile picture URL

    sub

    LinkedIn member ID

    email

    User's email address

    email_verified

    Email verification status

    name

    Full name

    given_name

    First name

    family_name

    Last name

    LinkedIn Developersarrow-up-right
    Configure Google
    Manage Linked Accounts
    docker run -d -p 6379:6379 redis:7-alpine
    brew install redis
    brew services start redis
    sudo apt install redis-server
    sudo systemctl enable redis
    sudo systemctl start redis
    REDIS_URL=redis://localhost:6379
    REDIS_URL=redis://:password@localhost:6379
    REDIS_URL=redis://localhost:6379/1
    # redis.conf
    
    # Memory
    maxmemory 256mb
    maxmemory-policy allkeys-lru
    
    # Persistence
    save 900 1
    save 300 10
    save 60 10000
    
    # Security
    requirepass your_redis_password
    bind 127.0.0.1
    
    # Performance
    tcp-keepalive 300
    REDIS_URL=rediss://user:password@redis.example.com:6380
    version: '3.8'
    
    services:
      authority:
        image: azutoolkit/authority
        environment:
          - REDIS_URL=redis://redis:6379
        depends_on:
          - redis
    
      redis:
        image: redis:7-alpine
        command: redis-server --requirepass ${REDIS_PASSWORD}
        volumes:
          - redis_data:/data
        restart: unless-stopped
    
    volumes:
      redis_data:
    REDIS_URL=redis://sentinel1:26379,sentinel2:26379,sentinel3:26379/mymaster
    REDIS_URL=redis://node1:6379,node2:6379,node3:6379
    authority:session:{session_id} -> {user_id, created_at, ...}
    authority:token:{token_hash} -> {user_id, scope, exp, ...}
    authority:ratelimit:{ip}:{endpoint} -> count
    redis-cli
    
    # Check memory usage
    INFO memory
    
    # List keys
    KEYS authority:*
    
    # Monitor commands
    MONITOR
    redis-cli -h localhost -p 6379 ping
    redis-cli ping
    redis-cli -a your_password ping
    redis-cli INFO memory
    GOOGLE_OAUTH_ENABLED=true
    GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
    GOOGLE_CLIENT_SECRET=your-client-secret
    <a href="https://your-authority-domain/auth/google" class="btn-google">
      <svg><!-- Google icon --></svg>
      Sign in with Google
    </a>
    <a href="https://your-authority-domain/auth/google?forward_url=BASE64_ENCODED_URL">
      Sign in with Google
    </a>
    const forwardUrl = btoa('https://your-app.com/dashboard');
    const googleAuthUrl = `https://your-authority-domain/auth/google?forward_url=${forwardUrl}`;
    https://your-authority-domain/auth/google/callback
    GITHUB_OAUTH_ENABLED=true
    GITHUB_CLIENT_ID=your-github-client-id
    GITHUB_CLIENT_SECRET=your-github-client-secret
    <a href="https://your-authority-domain/auth/github" class="btn-github">
      <svg><!-- GitHub icon --></svg>
      Sign in with GitHub
    </a>
    const forwardUrl = btoa('https://your-app.com/dashboard');
    const githubAuthUrl = `https://your-authority-domain/auth/github?forward_url=${forwardUrl}`;
    https://your-authority-domain/auth/github/callback
    https://your-authority-domain/auth/linkedin/callback
    LINKEDIN_OAUTH_ENABLED=true
    LINKEDIN_CLIENT_ID=your-linkedin-client-id
    LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
    <a href="https://your-authority-domain/auth/linkedin" class="btn-linkedin">
      <svg><!-- LinkedIn icon --></svg>
      Sign in with LinkedIn
    </a>
    const forwardUrl = btoa('https://your-app.com/dashboard');
    const linkedinAuthUrl = `https://your-authority-domain/auth/linkedin?forward_url=${forwardUrl}`;
    https://your-authority-domain/auth/linkedin/callback
    Monitor audit logs for MFA disable events
    Account Lockout
    Password Policies
    Audit Logging
    MFA Setup
    Profile data - Access user profile information

    Apple

    name, email

    Privacy-focused, email relay option

    Add login button to your application
    Consider requiring email verification
  • Implement account linking carefully to prevent account takeover

  • Google

    email, profile, openid

    Email verification, profile picture

    GitHub

    user:email, read:user

    Email, username, avatar

    Facebook

    email, public_profile

    Email, name, profile picture

    LinkedIn

    openid, profile, email

    Professional profile data

    Configure GitHub
    Configure Facebook
    Configure LinkedIn
    Configure Apple
    Manage Linked Accounts
    Configure Google
    Manage Linked Accounts
  • Unlink social providers they no longer want

  • Sign in with any linked provider

  • hashtag
    Linking Accounts

    hashtag
    Automatic Linking

    When a user signs in with a social provider:

    1. New user: Account created automatically with social provider linked

    2. Existing user (same email): Social provider linked to existing account

    3. Signed-in user: Social provider added to their account

    hashtag
    Manual Linking

    For signed-in users to add a social provider:

    The user will:

    1. Authenticate with the social provider

    2. Return to Authority with provider linked

    3. Continue with their existing session

    hashtag
    Unlinking Accounts

    hashtag
    API Endpoint

    Request:

    Response (Success):

    Response (Error):

    hashtag
    Safety Checks

    Authority prevents unlinking when:

    • It's the user's only login method

    • User has no password set

    • Would leave account inaccessible

    Solution: User must set a password or link another provider first.

    hashtag
    User Interface

    hashtag
    Account Settings Page

    Show users their linked accounts:

    hashtag
    Example UI

    hashtag
    JavaScript Handler

    hashtag
    Security Considerations

    hashtag
    Account Takeover Prevention

    When linking accounts, Authority verifies:

    1. Email match: Social account email matches existing account

    2. Session valid: User is properly authenticated

    3. State parameter: CSRF protection via state validation

    hashtag
    Multiple Accounts Warning

    If a social account is already linked to a different user:

    The user must unlink from the other account first.

    hashtag
    Audit Logging

    All link/unlink operations are recorded:

    hashtag
    Data Stored

    For each linked social account:

    Field
    Description

    provider

    Provider name (google, github, etc.)

    provider_user_id

    Unique ID from provider

    email

    Email from provider

    name

    Name from provider

    avatar_url

    Profile picture URL

    access_token

    Provider access token (encrypted)

    hashtag
    Best Practices

    1. Show linked status - Display which providers are connected

    2. Confirm unlinking - Require user confirmation

    3. Explain consequences - Warn if unlinking removes login method

    4. Update UI immediately - Reflect changes without page reload

    5. Log actions - Track for security auditing

    hashtag
    Next Steps

    • Configure Google - Set up providers

    • Enable MFA - Additional security

    • Audit Logging - Track account changes

    Create and edit users

  • Configure scopes

  • View audit logs

  • Modify system settings

  • hashtag
    Admin Dashboard

    hashtag
    Create Admin User

    1. Navigate to Admin Dashboard → Users

    2. Click New User

    3. Fill in the form:

    Field
    Description

    Email

    Admin email address

    Name

    Display name

    Password

    Initial password

    Role

    Select Administrator

    1. Click Create

    User Management

    hashtag
    Promote Existing User

    1. Navigate to Users

    2. Select the user

    3. Click Edit

    4. Change Role to Administrator

    5. Click Save

    hashtag
    Command Line

    hashtag
    Create Admin via CLI

    hashtag
    Using Database Seed

    Create admin during initial setup:

    hashtag
    API

    hashtag
    Create Admin via API

    hashtag
    Update User Role

    hashtag
    Admin Roles

    Role
    Permissions

    user

    Profile management, OAuth authorizations

    admin

    Full system access

    super_admin

    Can create other admins

    hashtag
    Role Hierarchy

    hashtag
    Security Requirements

    hashtag
    MFA for Admins

    Require MFA for all admin accounts:

    hashtag
    Admin Password Policy

    Enforce stronger passwords for admins:

    hashtag
    IP Restrictions

    Limit admin access by IP:

    hashtag
    Audit Trail

    Admin actions are logged:

    Event
    Description

    admin.login

    Admin login

    admin.created

    New admin created

    admin.role_changed

    Role modified

    client.created

    Client created by admin

    settings.changed

    Settings modified

    hashtag
    Best Practices

    circle-check

    Do:

    • Use individual accounts (not shared)

    • Enable MFA for all admins

    • Regularly review admin list

    • Limit super_admin count

    circle-exclamation

    Avoid:

    • Shared admin credentials

    • Admin accounts without MFA

    • Unnecessary admin access

    • Using admin accounts for daily work

    hashtag
    Removing Admin Access

    hashtag
    Demote to User

    hashtag
    Deactivate Admin

    hashtag
    Next Steps

    • Manage Sessions - Session management

    • Password Reset - Reset passwords

    • Enable MFA - Secure admin accounts

    Configuration

    hashtag
    Environment Variables

    Variable
    Default
    Description

    LOCKOUT_THRESHOLD

    5

    Failed attempts before lockout

    LOCKOUT_DURATION

    30

    Lockout duration (minutes)

    ENABLE_AUTO_UNLOCK

    true

    Auto-unlock after duration

    hashtag
    Example Configuration

    hashtag
    Progressive Lockout

    For enhanced security, increase lockout duration with each lockout:

    hashtag
    Admin Dashboard

    hashtag
    View Locked Accounts

    1. Navigate to Admin Dashboard → Users

    2. Filter by Status: Locked

    hashtag
    Unlock an Account

    1. Select the locked user

    2. Click Unlock Account

    3. Optionally, send password reset email

    Admin Users

    hashtag
    Monitoring

    hashtag
    Audit Log Events

    Lockout events are logged:

    Event
    Description

    user.login_failed

    Failed login attempt

    user.locked

    Account locked

    user.unlocked

    Account unlocked (auto or manual)

    hashtag
    Alerts

    Set up alerts for lockout events:

    hashtag
    API Integration

    hashtag
    Check Lock Status

    Response:

    hashtag
    Unlock User

    hashtag
    IP-Based Lockout

    Optionally lock by IP address instead of account:

    This helps when attackers try multiple usernames from the same IP.

    hashtag
    Whitelist

    Exclude certain IPs from lockout:

    hashtag
    User Experience

    hashtag
    Locked Account Message

    Users see a clear message when locked:

    hashtag
    Reset Password Option

    Offer password reset as an alternative:

    hashtag
    Best Practices

    circle-info

    Recommended settings for most deployments:

    • Threshold: 5 attempts

    • Duration: 30 minutes

    • Auto-unlock: Enabled

    • Progressive lockout: Enabled

    circle-exclamation

    Avoid:

    • Very low thresholds (< 3) - frustrates legitimate users

    • Very long durations (> 24 hours) - enables denial of service

    • Permanent lockout - requires manual intervention

    hashtag
    Troubleshooting

    hashtag
    Users getting locked frequently

    • Check for password manager issues

    • Verify caps lock behavior

    • Consider increasing threshold

    hashtag
    Lockout not working

    • Verify environment variables are loaded

    • Check ENABLE_LOCKOUT is not set to false

    • Review audit logs for failed attempts

    hashtag
    Auto-unlock not happening

    • Verify ENABLE_AUTO_UNLOCK=true

    • Check server time synchronization

    • Review logs for errors

    hashtag
    Next Steps

    • Enable MFA - Multi-factor authentication

    • Password Policies - Password requirements

    • Audit Logging - Track security events

    Admin access to Authority dashboard
  • Apple Developer account ($99/year membership required)

  • hashtag
    Overview

    Apple Sign-In is more complex than other providers because:

    1. Uses JWT-based client authentication (not simple client secret)

    2. Requires Apple Developer Program membership

    3. Has strict UI requirements

    4. Offers email relay for privacy

    hashtag
    Step 1: Create App ID

    1. Go to Apple Developer Consolearrow-up-right

    2. Navigate to Certificates, Identifiers & Profiles

    3. Click Identifiers > + button

    4. Select App IDs > Continue

    5. Select App type > Continue

    6. Fill in details:

      • Description: Your app name

      • Bundle ID: com.yourcompany.yourapp (explicit, not wildcard)

    7. In Capabilities, enable Sign in with Apple

    8. Click Continue > Register

    hashtag
    Step 2: Create Services ID

    1. Click Identifiers > + button

    2. Select Services IDs > Continue

    3. Fill in details:

      • Description: Your service description

      • Identifier: com.yourcompany.yourapp.auth (this is your Client ID)

    4. Click Continue > Register

    5. Click on your new Services ID to configure:

      • Enable Sign in with Apple

      • Click Configure

    6. In configuration:

      • Primary App ID: Select your App ID from Step 1

      • Domains: your-authority-domain (without https://)

    hashtag
    Step 3: Create Private Key

    1. Navigate to Keys > + button

    2. Fill in details:

      • Key Name: Authority Sign-in Key

      • Enable Sign in with Apple

      • Click Configure and select your Primary App ID

    3. Click Continue > Register

    4. Download the key file (.p8) - you can only download it once!

    5. Note the Key ID displayed

    hashtag
    Step 4: Get Your Team ID

    Find your Team ID:

    • Go to Membership in Apple Developer Console

    • Your Team ID is displayed (10-character string)

    hashtag
    Step 5: Configure Authority

    You need four pieces of information:

    Setting
    Where to Find

    Client ID

    Services ID identifier (e.g., com.yourcompany.yourapp.auth)

    Team ID

    Membership page in Developer Console

    Key ID

    Shown when creating the key

    Private Key

    Contents of the .p8 file

    hashtag
    Using Admin Dashboard

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable Apple OAuth

    4. Enter your credentials:

      • Client ID: Your Services ID identifier

      • Team ID: Your Apple Team ID

      • Key ID: Your key identifier

    5. Save settings

    hashtag
    Using Environment Variables

    circle-exclamation

    Keep your private key secure. Never commit it to version control.

    hashtag
    Step 6: Add Login Button

    Apple has specific requirements for their button:

    See Apple's Human Interface Guidelinesarrow-up-right for button requirements.

    hashtag
    User Data Retrieved

    Field
    Description

    sub

    Unique Apple user ID

    email

    User's email (real or relay)

    email_verified

    Always true for Apple

    name

    Name (only on first auth)

    circle-info

    Apple only provides the user's name on the first authentication. Store it immediately.

    hashtag
    Email Relay

    Users can choose to hide their real email. Apple provides a relay address like:

    Emails sent to this address are forwarded to the user's real email.

    To send emails to relay addresses:

    1. Register your email domains in Apple Developer Console

    2. Configure SPF/DKIM for your sending domain

    hashtag
    Troubleshooting

    hashtag
    "Invalid client_id"

    Services ID not configured correctly.

    Solution:

    • Verify Services ID identifier matches your Client ID

    • Ensure Sign in with Apple is enabled on the Services ID

    • Check domain and return URL configuration

    hashtag
    "Invalid redirect_uri"

    Callback URL not registered.

    Solution:

    • In Services ID configuration, verify Return URL exactly matches:

    • Ensure domain is registered (without https://)

    hashtag
    "Invalid grant"

    Authorization code expired or already used.

    Solution: Apple codes expire quickly. Ensure your token exchange happens promptly.

    hashtag
    Name Not Retrieved

    Apple only provides name on first authentication.

    Solution: Store the name immediately on first login. If missed, user must revoke app access and re-authenticate.

    hashtag
    Security Considerations

    1. Protect your private key - Store securely, never in code

    2. Rotate keys periodically - Create new key, update config, then delete old key

    3. Handle relay emails - Test email delivery to relay addresses

    4. First-auth data - Cache name immediately as it's only provided once

    hashtag
    Next Steps

    • Configure Google - Simpler provider setup

    • Manage Linked Accounts - Account linking

  • Custom scopes - Application-specific access levels (read, write, admin)

  • hashtag
    Default Scopes

    Authority includes these standard scopes:

    Scope
    Description
    Claims

    openid

    OpenID Connect

    sub

    profile

    User profile

    name, family_name, given_name, picture

    email

    Email address

    email, email_verified

    address

    Postal address

    address

    hashtag
    Admin Dashboard

    hashtag
    Create Scope

    1. Navigate to Admin Dashboard → Scopes

    2. Click New Scope

    3. Fill in:

    Field
    Description

    Name

    Scope identifier (e.g., read)

    Description

    Human-readable description

    Default

    Include in all authorizations

    1. Click Create

    Scopes

    hashtag
    Edit Scope

    1. Select the scope

    2. Modify fields

    3. Click Save

    hashtag
    Delete Scope

    1. Select the scope

    2. Click Delete

    3. Confirm deletion

    circle-exclamation

    Deleting a scope may break existing client integrations.

    hashtag
    API Management

    hashtag
    Create Scope

    hashtag
    List Scopes

    Response:

    hashtag
    Update Scope

    hashtag
    Delete Scope

    hashtag
    Scope Naming Conventions

    hashtag
    Hierarchical Scopes

    Use colons to create hierarchies:

    hashtag
    Resource-Action Pattern

    Format: resource:action

    Scope
    Resource
    Action

    orders:read

    Orders

    Read

    orders:create

    Orders

    Create

    products:list

    Products

    List

    hashtag
    API Versioning

    Include version if needed:

    hashtag
    Assign Scopes to Clients

    When registering or updating a client:

    hashtag
    Scope Consent

    When users authorize a client, they see requested scopes:

    hashtag
    Validating Scopes

    hashtag
    At Token Issuance

    Authority validates that:

    1. Requested scopes exist

    2. Client is allowed to request them

    3. User consents to them

    hashtag
    In Your API

    Check scopes in access tokens:

    hashtag
    Default Scopes

    Set default scopes included in all authorizations:

    Or mark scopes as default in the admin dashboard.

    hashtag
    Scope Dependencies

    Define scopes that require other scopes:

    When admin is requested, read and write are automatically included.

    hashtag
    Best Practices

    circle-check

    Do:

    • Use specific scopes (not just read/write)

    • Document what each scope grants

    • Use hierarchical naming

    • Include scopes in API documentation

    circle-exclamation

    Avoid:

    • Overly broad scopes

    • Scopes that overlap

    • Changing scope meaning after deployment

    hashtag
    Next Steps

    • Register Client - Create OAuth clients

    • Rotate Secrets - Secret management

    • Protect Your API - Scope enforcement

    Rotate Secrets

    Manage and rotate OAuth client secrets for security.

    hashtag
    Why Rotate Secrets?

    • Secret may have been exposed

    • Regular security policy compliance

    • Employee departure

    • Security audit requirement

    hashtag
    Rotation Methods

    hashtag
    Method 1: Admin Dashboard

    1. Navigate to Admin Dashboard → OAuth Clients

    2. Select the client

    3. Click Rotate Secret

    hashtag
    Method 2: API

    Response:

    hashtag
    Zero-Downtime Rotation

    For production applications, use a two-step rotation:

    hashtag
    Step 1: Add New Secret

    Some systems support multiple active secrets. If Authority supports this:

    hashtag
    Step 2: Update Application

    Update your application to use the new secret.

    hashtag
    Step 3: Remove Old Secret

    hashtag
    Rotation Without Dual Secrets

    If you must rotate immediately:

    hashtag
    1. Prepare New Configuration

    Have the new secret ready to deploy.

    hashtag
    2. Rotate Secret

    hashtag
    3. Deploy Immediately

    Update your application within seconds:

    hashtag
    Automation

    hashtag
    Scheduled Rotation

    Create a rotation script:

    hashtag
    Cron Schedule

    hashtag
    Secret Expiration

    Configure secrets to expire automatically:

    Monitor expiring secrets:

    hashtag
    Audit Trail

    All secret rotations are logged:

    Event
    Description

    Query the audit log:

    hashtag
    Notification

    Set up alerts for secret events:

    hashtag
    Recovery

    hashtag
    Lost Secret

    If you lose a client secret:

    1. Log into admin dashboard

    2. Rotate to generate new secret

    3. Update application immediately

    hashtag
    Compromised Secret

    If a secret is compromised:

    1. Rotate immediately

    2. Review audit logs for unauthorized access

    3. Revoke any suspicious tokens

    4. Update application with new secret

    hashtag
    Best Practices

    circle-check

    Do:

    • Rotate secrets regularly (quarterly minimum)

    • Automate rotation process

    circle-exclamation

    Avoid:

    • Storing secrets in code

    • Sharing secrets via email/chat

    hashtag
    Next Steps

    • - Create OAuth clients

    • - Access control

    • - Track rotations

    Manage Sessions

    View and revoke user sessions in Authority.

    hashtag
    Overview

    Sessions represent active user logins. Each session tracks:

    • Device information

    • IP address

    • Login time

    • Last activity

    hashtag
    User Self-Service

    hashtag
    View Active Sessions

    Users can see their sessions in the profile:

    1. Click profile name

    2. Select Security Settings

    3. View Active Sessions

    hashtag
    Revoke Session

    1. Find the session in the list

    2. Click Revoke

    3. Confirm revocation

    The session is immediately invalidated.

    hashtag
    Admin Management

    hashtag
    View User Sessions

    Response:

    hashtag
    Revoke Specific Session

    hashtag
    Revoke All User Sessions

    This forces the user to re-authenticate on all devices.

    hashtag
    Session Settings

    hashtag
    Configuration

    Variable
    Default
    Description

    hashtag
    Single Session Mode

    Force users to have only one active session:

    When enabled, logging in from a new device revokes existing sessions.

    hashtag
    Idle Timeout

    End sessions after inactivity:

    Users are logged out after 30 minutes of inactivity.

    hashtag
    Session Information

    Each session captures:

    Field
    Description

    hashtag
    Security Alerts

    hashtag
    Notify on New Session

    Email users about new logins:

    Email content:

    hashtag
    Suspicious Session Detection

    Alert on unusual patterns:

    • Login from new location

    • Multiple simultaneous sessions

    • Login outside business hours

    hashtag
    Bulk Session Management

    hashtag
    Revoke All Sessions (System-Wide)

    For security incidents:

    hashtag
    Revoke by Criteria

    hashtag
    Session in OAuth Flow

    hashtag
    Token and Session Relationship

    Revoking a session can optionally revoke associated tokens:

    hashtag
    SSO Session

    For single sign-on, a single session can authorize multiple clients.

    hashtag
    Monitoring

    hashtag
    Active Session Count

    hashtag
    Session Audit Events

    Event
    Description

    hashtag
    Next Steps

    • - Admin accounts

    • - Reset passwords

    • - Track sessions

    API

    Complete reference for all Authority API endpoints.

    hashtag
    OAuth 2.0 Endpoints

    hashtag
    Authorization Endpoint

    GET /authorize

    Initiates the OAuth 2.0 authorization flow. Displays consent form to user.

    Query Parameters:

    Parameter
    Required
    Description

    Example:

    Response: Redirects to login page, then to redirect_uri with code and state parameters.

    Error Response: Redirects to redirect_uri with error, error_description, and state parameters.


    POST /authorize

    Process user consent for authorization request.

    Request Body (Form-encoded):

    Parameter
    Required
    Description

    Response: 302 redirect to redirect_uri?code=<code>&state=<state>


    hashtag
    Token Endpoint

    POST /token

    Exchange authorization code, refresh token, or client credentials for access tokens.

    Headers:

    Header
    Description

    Request Body (Authorization Code):

    Parameter
    Required
    Description

    Request Body (Refresh Token):

    Parameter
    Required
    Description

    Request Body (Client Credentials):

    Parameter
    Required
    Description

    Request Body (Password Grant - Legacy):

    Parameter
    Required
    Description

    Response: 201 Created


    hashtag
    Token Introspection

    POST /oauth/introspect

    Validate a token and get its metadata (RFC 7662).

    Headers:

    Header
    Description

    Request Body:

    Parameter
    Required
    Description

    Response (Active Token): 200 OK

    Response (Inactive Token):


    hashtag
    Token Revocation

    POST /oauth/revoke

    Revoke an access or refresh token (RFC 7009).

    Headers:

    Header
    Description

    Request Body:

    Parameter
    Required
    Description

    Response: 200 OK (always succeeds per RFC 7009)


    hashtag
    Device Authorization

    POST /device/code

    Start device authorization flow (RFC 8628).

    Headers:

    Header
    Description

    Request Body:

    Parameter
    Required
    Description

    Response: 201 Created


    hashtag
    Device Token Polling

    POST /device/token

    Poll for device authorization completion.

    Request Body:

    Parameter
    Required
    Description

    Response (Pending): 400 Bad Request

    Response (Success): 201 Created


    hashtag
    Device Activation

    GET /activate

    Display device activation form for user to enter code.

    Query Parameters:

    Parameter
    Required
    Description

    Response: HTML form for user to enter device code.


    POST /activate

    Process device activation.

    Request Body:

    Parameter
    Required
    Description

    Response: HTML confirmation page.


    hashtag
    OpenID Connect Endpoints

    hashtag
    UserInfo

    GET /oauth2/userinfo

    Get authenticated user's claims.

    Headers:

    Header
    Description

    Response: 200 OK


    hashtag
    Discovery

    GET /.well-known/openid-configuration

    OpenID Connect discovery document.

    Response: 200 OK

    Caching: public, max-age=3600


    hashtag
    JWKS

    GET /.well-known/jwks.json

    JSON Web Key Set for token verification.

    Response: 200 OK

    Caching: public, max-age=3600


    hashtag
    Dynamic Client Registration

    hashtag
    Register Client

    POST /register

    Dynamically register an OAuth client (RFC 7591).

    Request Body:

    Response: 201 Created

    Validation:

    • redirect_uris must use HTTPS (except localhost for development)

    • redirect_uris must not contain URL fragments


    hashtag
    Authentication Endpoints

    hashtag
    Sign In Form

    GET /signin

    Display sign-in form.

    Query Parameters:

    Parameter
    Required
    Description

    Response: HTML sign-in form


    hashtag
    Sign In

    POST /signin

    Authenticate user.

    Request Body (Form-encoded):

    Parameter
    Required
    Description

    Response (Success): 302 redirect to profile or forward_url

    Response (MFA Required): 302 redirect to /mfa/verify

    Response (Account Locked): 423 Locked with Retry-After header

    Response (Invalid Credentials): 401 Unauthorized


    hashtag
    Sign Out

    POST /signout

    End user session.

    Response: 302 redirect to sign-in page


    hashtag
    Account Endpoints

    hashtag
    Forgot Password

    GET /forgot-password

    Display forgot password form.


    POST /forgot-password

    Request password reset email.

    Request Body (Form-encoded):

    Parameter
    Required
    Description

    Response: Always shows success (prevents email enumeration)


    hashtag
    Password Reset

    POST /account/password/reset

    Request password reset token.

    Request Body:

    Parameter
    Required
    Description

    Response: 200 OK (always, to prevent enumeration)


    POST /account/password/confirm

    Complete password reset.

    Request Body:

    Parameter
    Required
    Description

    Response (Success): 200 OK

    Response (Error): 400 Bad Request


    hashtag
    Email Verification

    POST /account/email/verify

    Verify email address.

    Request Body:

    Parameter
    Required
    Description

    Response: 200 OK or 400 Bad Request


    POST /account/email/resend

    Resend verification email.

    Request Body:

    Parameter
    Required
    Description

    Response: 200 OK


    hashtag
    MFA Endpoints

    hashtag
    MFA Setup

    GET /mfa/setup

    Display MFA setup with QR code.

    Response: HTML page with:

    • QR code for authenticator app

    • Secret key (manual entry)

    • Backup codes

    Authentication Required: Yes (session)


    hashtag
    Enable MFA

    POST /mfa/enable

    Enable MFA for account.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect

    Authentication Required: Yes (session)


    hashtag
    Verify MFA

    POST /mfa/verify

    Verify MFA code during login.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect to profile


    hashtag
    Disable MFA

    POST /mfa/disable

    Disable MFA for account.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect

    Authentication Required: Yes (session)


    hashtag
    User Profile

    hashtag
    View Profile

    GET /profile

    Display user profile page.

    Response: HTML profile page with:

    • User details

    • Email verification status

    • MFA status

    • Connected social accounts

    Authentication Required: Yes (session)


    hashtag
    Health Check

    GET /health_check

    Check server health.

    Response: 200 OK


    hashtag
    Social Login

    See for social authentication endpoints.


    hashtag
    Next Steps

    • - Social authentication endpoints

    • - Error response reference

    • - Rate limiting details

    OAuth Clients

    Create and configure OAuth clients in Authority.

    hashtag
    Overview

    OAuth clients represent applications that can request access tokens. Each client has:

    • Client ID - Public identifier

    • Client Secret - Confidential key (for confidential clients)

    • Redirect URIs - Allowed callback URLs

    • Scopes - Permitted access levels

    hashtag
    Client Types

    Type
    Description
    Use Case

    hashtag
    Admin Dashboard

    hashtag
    Create Client

    1. Navigate to Admin Dashboard → OAuth Clients

    2. Click New Client

    3. Fill in the form:

    Field
    Description
    1. Click Create

    hashtag
    Client Credentials

    After creation, you'll receive:

    • Client ID: abc123def456...

    • Client Secret: xyz789ghi012... (save this - shown only once)

    circle-exclamation

    Store the client secret securely. It cannot be retrieved later.

    hashtag
    API Registration

    hashtag
    Create Client via API

    Response:

    hashtag
    Get Client Details

    hashtag
    Update Client

    hashtag
    Delete Client

    hashtag
    Redirect URI Configuration

    hashtag
    Best Practices

    Do
    Don't

    hashtag
    Valid Examples

    hashtag
    Invalid Examples

    hashtag
    Grant Types

    Configure which OAuth flows the client can use:

    Grant Type
    Value
    Use Case

    hashtag
    Authentication Methods

    Method
    Description

    hashtag
    Scopes

    Assign allowed scopes:

    Clients can only request scopes they're allowed to use.

    hashtag
    Client Metadata

    Store additional client information:

    hashtag
    Testing Your Client

    After registration, test the authorization flow:

    hashtag
    Next Steps

    • - Create custom scopes

    • - Secret management

    • - Complete tutorial

    Customization

    Modify Authority's appearance with Jinja templates.

    hashtag
    Overview

    Authority uses Crinja (Jinja2-compatible) templates for all pages:

    • Login / Registration

    • OAuth consent

    • Password reset

    • Admin dashboard

    • User profile

    hashtag
    Template Structure

    hashtag
    UI Components

    hashtag
    Template Syntax

    hashtag
    Variables

    hashtag
    Conditionals

    hashtag
    Loops

    hashtag
    Template Inheritance

    Base layout (layout.html):

    Page template (signin.html):

    hashtag
    Available Variables

    hashtag
    Sign In Page

    Variable
    Description

    hashtag
    Authorization Page

    Variable
    Description

    hashtag
    Device Activation

    Variable
    Description

    hashtag
    Styling

    hashtag
    CSS Variables

    hashtag
    Custom Theme

    Create a custom theme:

    hashtag
    Screenshots

    hashtag
    Landing Page

    hashtag
    Sign In

    hashtag
    Admin Dashboard

    hashtag
    Configuration

    hashtag
    Custom Template Path

    hashtag
    Reload Templates

    In development, templates reload automatically. In production:

    hashtag
    Best Practices

    circle-check

    Do:

    • Keep custom templates in version control

    • Test on multiple screen sizes

    circle-exclamation

    Avoid:

    • Removing hidden form fields

    • Modifying form action URLs

    hashtag
    Next Steps

    • - Customize emails

    • - Logo and colors

    • - Full reference

    Branding

    Customize Authority's visual identity for your organization.

    hashtag
    Logo

    hashtag
    Replace Logo

    Place your logo in the public directory:

    hashtag
    Logo Requirements

    Type
    Size
    Format

    hashtag
    Template Usage

    hashtag
    Colors

    hashtag
    Primary Colors

    Customize in public/css/styles.css:

    hashtag
    Dark Theme (Default)

    hashtag
    Light Theme

    hashtag
    Application Name

    hashtag
    Configuration

    hashtag
    Template Usage

    hashtag
    Custom Fonts

    hashtag
    Google Fonts

    hashtag
    Self-Hosted Fonts

    hashtag
    Login Page

    hashtag
    Background Image

    hashtag
    Login Card

    hashtag
    Email Branding

    hashtag
    Email Header

    hashtag
    Email Footer

    hashtag
    OAuth Consent Page

    hashtag
    Client Branding

    Show client logos on consent page:

    hashtag
    Scope Icons

    hashtag
    Configuration Reference

    hashtag
    Best Practices

    circle-check

    Do:

    • Use consistent brand colors

    • Maintain good contrast ratios

    circle-exclamation

    Avoid:

    • Low contrast text

    • Very small fonts

    hashtag
    Next Steps

    • - Template customization

    • - Email branding

    • - HTTPS setup

    Email Templates

    Configure transactional emails sent by Authority.

    hashtag
    Email Types

    Authority sends these transactional emails:

    Email
    Trigger

    hashtag
    Template Location

    hashtag
    Template Structure

    hashtag
    Base Template

    emails/base.html:

    hashtag
    Password Reset Email

    emails/password-reset.html:

    hashtag
    Welcome Email

    emails/welcome.html:

    hashtag
    Available Variables

    hashtag
    All Emails

    Variable
    Description

    hashtag
    Password Reset

    Variable
    Description

    hashtag
    Email Verification

    Variable
    Description

    hashtag
    New Login Alert

    Variable
    Description

    hashtag
    Configuration

    hashtag
    SMTP Settings

    hashtag
    Email Settings

    hashtag
    Testing Emails

    hashtag
    Preview in Browser

    hashtag
    Send Test Email

    hashtag
    HTML Email Best Practices

    circle-info

    Email HTML is limited:

    • Use inline CSS

    • Use tables for layout

    hashtag
    Compatible Styles

    hashtag
    Localization

    hashtag
    Multiple Languages

    Configure default locale:

    hashtag
    Troubleshooting

    hashtag
    Emails Not Sending

    • Check SMTP configuration

    • Verify credentials

    • Check spam filters

    • Review server logs

    hashtag
    Broken Layout

    • Use inline CSS

    • Test with email preview tools

    • Check image URLs are absolute

    hashtag
    Images Not Loading

    • Use absolute URLs

    • Host images on accessible server

    • Check HTTPS requirements

    hashtag
    Next Steps

    • - Customize pages

    • - Logo and colors

    • - Email settings

    Authorization Code

    The authorization code grant is used when an application exchanges an authorization code for an access token.

    hashtag
    Overview

    This flow is for web applications with a server-side component that can securely store the client secret.

    hashtag
    Flow Diagram

    hashtag
    Authorization Request

    GET /authorize

    Redirect the user to the authorization endpoint.

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Response

    User is redirected to redirect_uri with:

    circle-info

    Always verify the state parameter matches what you sent to prevent CSRF attacks.

    hashtag
    Token Request

    POST /token

    Exchange the authorization code for tokens.

    hashtag
    Headers

    Header
    Value

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Response

    hashtag
    Complete Example

    hashtag
    Node.js

    hashtag
    Python

    hashtag
    Security Considerations

    1. Always validate state - Prevents CSRF attacks

    2. Use HTTPS - Never transmit tokens over HTTP

    3. Store secrets securely - Never expose client_secret

    hashtag
    Next Steps

    • - For public clients

    • - Token renewal

    • - Response format

    Error Codes

    Reference for Authority API error responses.

    hashtag
    Error Response Format

    All errors follow this format:

    hashtag

    Authorization Code + PKCE

    PKCE (Proof Key for Code Exchange) extends the authorization code flow for public clients that cannot securely store a client secret.

    hashtag
    Overview

    PKCE protects against authorization code interception attacks by using a dynamically generated secret.

    Refresh Tokens

    Refresh tokens allow clients to obtain new access tokens without user interaction.

    hashtag
    Overview

    When an access token expires, use the refresh token to get a new one without requiring the user to re-authenticate.

    hashtag

    OAuth 2.0

    Complete reference for OAuth 2.0 grant types supported by Authority.

    hashtag
    Grant Type Overview

    hashtag
    Available Grant Types

    abc12-def34
    ghi56-jkl78
    mno90-pqr12
    ...
    REQUIRE_ADMIN_MFA=true
    REQUIRE_MFA=true
    MFA_GRACE_PERIOD_DAYS=7
    GET /api/users/{id}
    Authorization: Bearer {admin_token}
    {
      "id": "user-uuid",
      "email": "user@example.com",
      "mfa_enabled": true,
      "mfa_configured_at": "2024-01-15T10:30:00Z"
    }
    // After successful password authentication
    if (user.mfa_enabled) {
      // Show TOTP input form
      showMFAPrompt();
    } else if (settings.require_mfa) {
      // Redirect to MFA setup
      redirectToMFASetup();
    }
    <a href="https://your-authority-domain/auth/google?link=true">
      Connect Google Account
    </a>
    POST /auth/{provider}/unlink
    curl -X POST https://your-authority-domain/auth/google/unlink \
      -H "Cookie: session=..." \
      -H "Content-Type: application/json"
    {
      "success": true,
      "message": "Google account unlinked successfully"
    }
    {
      "error": "cannot_unlink",
      "message": "Cannot unlink the only login method. Set a password first."
    }
    // Fetch user's linked providers
    const response = await fetch('https://your-authority-domain/userinfo', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });
    
    const userInfo = await response.json();
    // Check for linked providers in user profile
    <div class="linked-accounts">
      <h3>Linked Accounts</h3>
    
      <div class="provider">
        <span class="icon">🔵</span>
        <span class="name">Google</span>
        <span class="email">user@gmail.com</span>
        <button onclick="unlinkProvider('google')">Disconnect</button>
      </div>
    
      <div class="provider">
        <span class="icon">⚫</span>
        <span class="name">GitHub</span>
        <span class="status">Not connected</span>
        <a href="/auth/github?link=true">Connect</a>
      </div>
    </div>
    async function unlinkProvider(provider) {
      if (!confirm(`Disconnect ${provider}?`)) return;
    
      try {
        const response = await fetch(`/auth/${provider}/unlink`, {
          method: 'POST',
          credentials: 'include'
        });
    
        const result = await response.json();
    
        if (result.success) {
          // Refresh UI
          location.reload();
        } else {
          alert(result.message);
        }
      } catch (error) {
        alert('Failed to unlink account');
      }
    }
    {
      "error": "already_linked",
      "message": "This social account is linked to another user"
    }
    {
      "event": "social_account_linked",
      "user_id": "user-uuid",
      "provider": "google",
      "provider_user_id": "google-user-id",
      "timestamp": "2024-01-15T10:30:00Z"
    }
    
    {
      "event": "social_account_unlinked",
      "user_id": "user-uuid",
      "provider": "github",
      "timestamp": "2024-01-15T11:45:00Z"
    }
    crystal run src/tasks/create_admin.cr -- \
      --email admin@example.com \
      --name "Admin User" \
      --password "SecurePassword123"
    ADMIN_EMAIL=admin@example.com \
    ADMIN_PASSWORD=SecurePassword123 \
    crystal run src/db/seed.cr
    POST /api/users
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "email": "newadmin@example.com",
      "name": "New Admin",
      "password": "SecurePassword123",
      "role": "admin"
    }
    PATCH /api/users/{id}
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "role": "admin"
    }
    super_admin
        └── admin
            └── user
    REQUIRE_ADMIN_MFA=true
    ADMIN_PASSWORD_MIN_LENGTH=16
    ADMIN_PASSWORD_EXPIRY_DAYS=30
    ADMIN_ALLOWED_IPS=10.0.0.0/8,192.168.1.100
    PATCH /api/users/{id}
    Content-Type: application/json
    Authorization: Bearer {super_admin_token}
    
    {
      "role": "user"
    }
    PATCH /api/users/{id}
    Content-Type: application/json
    Authorization: Bearer {super_admin_token}
    
    {
      "active": false
    }
    # Lock after 5 failed attempts
    LOCKOUT_THRESHOLD=5
    
    # Lock for 30 minutes
    LOCKOUT_DURATION=30
    
    # Auto-unlock enabled
    ENABLE_AUTO_UNLOCK=true
    # First lockout: 15 minutes
    # Second lockout: 30 minutes
    # Third lockout: 60 minutes
    # Fourth+: 120 minutes
    PROGRESSIVE_LOCKOUT=true
    # Example: Send alert on multiple lockouts
    grep "user.locked" /var/log/authority/audit.log | alert-script
    GET /api/users/{id}
    Authorization: Bearer {admin_token}
    {
      "id": "user-uuid",
      "email": "user@example.com",
      "locked": true,
      "locked_at": "2024-01-15T10:30:00Z",
      "failed_attempts": 5,
      "unlock_at": "2024-01-15T11:00:00Z"
    }
    POST /api/users/{id}/unlock
    Authorization: Bearer {admin_token}
    LOCKOUT_BY_IP=true
    IP_LOCKOUT_THRESHOLD=10
    LOCKOUT_WHITELIST=192.168.1.0/24,10.0.0.0/8
    Your account has been temporarily locked due to multiple failed login attempts.
    Please try again in 30 minutes or contact support.
    Forgot your password? [Reset Password]
    https://your-authority-domain/auth/apple/callback
    APPLE_OAUTH_ENABLED=true
    APPLE_CLIENT_ID=com.yourcompany.yourapp.auth
    APPLE_TEAM_ID=XXXXXXXXXX
    APPLE_KEY_ID=XXXXXXXXXX
    APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
    MIGTAgEA...
    -----END PRIVATE KEY-----"
    <a href="https://your-authority-domain/auth/apple" class="btn-apple">
      <svg><!-- Apple logo --></svg>
      Sign in with Apple
    </a>
    abc123@privaterelay.appleid.com
    POST /api/scopes
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "name": "documents:read",
      "description": "Read access to documents",
      "default": false
    }
    GET /api/scopes
    Authorization: Bearer {admin_token}
    {
      "data": [
        {
          "id": "scope-uuid",
          "name": "documents:read",
          "description": "Read access to documents",
          "default": false
        }
      ]
    }
    PATCH /api/scopes/{id}
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "description": "Updated description"
    }
    DELETE /api/scopes/{id}
    Authorization: Bearer {admin_token}
    documents:read
    documents:write
    documents:delete
    users:read
    users:write
    api:v1:read
    api:v2:read
    {
      "client_name": "My App",
      "scope": "openid profile email documents:read documents:write"
    }
    My App is requesting access to:
    
    ✓ Your profile information
    ✓ Your email address
    ✓ Read your documents
    ✓ Modify your documents
    
    [Allow] [Deny]
    const token = jwt.verify(accessToken, publicKey);
    const scopes = token.scope.split(' ');
    
    if (!scopes.includes('documents:read')) {
      throw new ForbiddenError('Insufficient scope');
    }
    DEFAULT_SCOPES=openid profile email
    {
      "name": "admin",
      "requires": ["read", "write"]
    }
    Return URLs: https://your-authority-domain/auth/apple/callback
  • Click Save

  • Private Key: Paste contents of .p8 file (including BEGIN/END lines)

    refresh_token

    Provider refresh token (encrypted)

    token_expires_at

    Token expiration time

    phone

    Phone number

    phone_number, phone_number_verified

    offline_access

    Refresh tokens

    -

    Copy the new secret immediately
  • Update your application configuration

  • The old secret is immediately invalidated

  • Use secrets management (Vault, AWS Secrets Manager)

  • Monitor for secret exposure

  • Long-lived secrets without rotation

  • Manual rotation in production

  • client.secret_rotated

    Secret was rotated

    client.secret_expired

    Secret expired

    Register Client
    Configure Scopes
    Audit Logging

    SESSION_DURATION_DAYS

    7

    Maximum session lifetime

    IDLE_TIMEOUT_MINUTES

    30

    Timeout after inactivity

    SINGLE_SESSION

    false

    Allow only one active session

    ip_address

    Client IP at login

    user_agent

    Browser/app information

    device

    Parsed device type

    location

    Approximate location (if enabled)

    created_at

    Login timestamp

    last_activity

    Last request timestamp

    session.created

    New login

    session.refreshed

    Session activity

    session.revoked

    Session ended

    session.expired

    Session timed out

    Create Admin
    Password Reset
    Audit Logging
    User Profile

    state

    Yes

    CSRF protection token

    code_challenge

    For PKCE

    Base64URL-encoded challenge

    code_challenge_method

    For PKCE

    S256 or plain

    nonce

    For OIDC

    Replay prevention token

    state

    Yes

    CSRF protection token

    consent_action

    Yes

    approve or deny

    code_challenge

    For PKCE

    Base64URL-encoded challenge

    code_challenge_method

    For PKCE

    S256 or plain

    nonce

    For OIDC

    Replay prevention token

    Active sessions

    OAuth 2.0 Flows - Grant type specifications

    response_type

    Yes

    Must be code

    client_id

    Yes

    The client identifier

    redirect_uri

    Yes

    Callback URL (must match registered URI)

    scope

    Yes

    response_type

    Yes

    Must be code

    client_id

    Yes

    The client identifier

    redirect_uri

    Yes

    Callback URL

    scope

    Yes

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    authorization_code

    code

    Yes

    Authorization code

    redirect_uri

    Yes

    Same as authorization request

    code_verifier

    For PKCE

    grant_type

    Yes

    refresh_token

    refresh_token

    Yes

    Refresh token

    grant_type

    Yes

    client_credentials

    scope

    Optional

    Requested scopes

    grant_type

    Yes

    password

    username

    Yes

    User's email

    password

    Yes

    User's password

    scope

    Optional

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    token

    Yes

    Token to introspect

    token_type_hint

    Optional

    access_token or refresh_token

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    token

    Yes

    Token to revoke

    token_type_hint

    Optional

    access_token or refresh_token

    Content-Type

    application/x-www-form-urlencoded

    client_id

    Yes

    Client identifier

    grant_type

    Yes

    urn:ietf:params:oauth:grant-type:device_code

    client_id

    Yes

    Client identifier

    code

    Yes

    Device code from /device/code

    user_code

    Optional

    Pre-fill user code

    user_code

    Yes

    User code from device

    Authorization

    Bearer {access_token}

    forward_url

    Optional

    Base64-encoded redirect URL after login

    email

    Yes

    User's email address

    password

    Yes

    User's password

    forward_url

    Optional

    Base64-encoded redirect URL

    email

    Yes

    User's email address

    email

    Yes

    User's email address

    token

    Yes

    Reset token from email

    password

    Yes

    New password

    token

    Yes

    Verification token from email

    email

    Yes

    Email address

    totp_code

    Yes

    6-digit verification code

    totp_code

    Yes

    6-digit code from authenticator

    password

    Yes

    Current password for verification

    Social Login API
    Social Login
    Error Codes
    Rate Limits

    Space-separated scopes

    Space-separated scopes

    Original code verifier

    Requested scopes

    Device Code

    urn:ietf:params:oauth:grant-type:device_code

    IoT/CLI

    Confidential

    Can securely store secrets

    Server-side apps

    Public

    Cannot store secrets

    Mobile apps, SPAs

    Name

    Display name for the client

    Type

    Confidential or Public

    Redirect URIs

    Callback URLs (one per line)

    Scopes

    Allowed scopes

    Grant Types

    Enabled OAuth flows

    Use exact URLs

    Use wildcards

    Use HTTPS in production

    Use HTTP in production

    Register all environments

    Use localhost in production

    Authorization Code

    authorization_code

    Web apps

    PKCE

    authorization_code

    Mobile/SPA

    Client Credentials

    client_credentials

    Service-to-service

    Refresh Token

    refresh_token

    Token renewal

    client_secret_basic

    HTTP Basic auth

    client_secret_post

    Secret in body

    none

    Public client (no secret)

    Configure Scopes
    Rotate Secrets
    First OAuth Integration
    OAuth Clients

    Test on mobile devices

  • Include alt text for images

  • Overly complex layouts

  • Slow-loading images

  • Logo

    200x50px

    PNG, SVG

    Logo (dark)

    200x50px

    PNG, SVG

    Favicon

    32x32px

    ICO, PNG

    Apple Touch

    180x180px

    PNG

    UI Templates
    Email Templates
    SSL Certificates

    Avoid JavaScript

  • Test across email clients

  • Welcome

    New user registration

    Email Verification

    Verify email address

    Password Reset

    Reset password request

    MFA Enabled

    Two-factor authentication enabled

    New Login

    Login from new device

    base_url

    Authority base URL

    app_name

    Application name

    company_address

    Company address

    user.name

    User's name

    user.email

    User's email

    reset_url

    Password reset link

    expiry_minutes

    Link expiry time

    verification_url

    Verification link

    expiry_hours

    Link expiry time

    device

    Device description

    location

    Approximate location

    ip_address

    Client IP

    login_time

    Login timestamp

    UI Templates
    Branding
    Environment Variables
    POST /register/{client_id}/renew_secret
    Authorization: Bearer {admin_token}
    {
      "client_id": "abc123def456",
      "client_secret": "new_secret_xyz789",
      "client_secret_expires_at": 0
    }
    POST /register/{client_id}/secrets
    Authorization: Bearer {admin_token}
    
    {
      "action": "add"
    }
    DELETE /register/{client_id}/secrets/{old_secret_id}
    Authorization: Bearer {admin_token}
    POST /register/{client_id}/renew_secret
    Authorization: Bearer {admin_token}
    # Example: Update Kubernetes secret
    kubectl create secret generic oauth-secret \
      --from-literal=client-secret=NEW_SECRET \
      --dry-run=client -o yaml | kubectl apply -f -
    
    # Rolling restart
    kubectl rollout restart deployment/my-app
    #!/bin/bash
    # rotate-secrets.sh
    
    CLIENT_ID="abc123def456"
    ADMIN_TOKEN="your_admin_token"
    AUTHORITY_URL="https://auth.example.com"
    
    # Rotate secret
    NEW_SECRET=$(curl -s -X POST \
      "${AUTHORITY_URL}/register/${CLIENT_ID}/renew_secret" \
      -H "Authorization: Bearer ${ADMIN_TOKEN}" \
      | jq -r '.client_secret')
    
    # Update Kubernetes secret
    kubectl create secret generic oauth-secret \
      --from-literal=client-secret="${NEW_SECRET}" \
      --dry-run=client -o yaml | kubectl apply -f -
    
    # Restart application
    kubectl rollout restart deployment/my-app
    
    # Notify team
    echo "Client secret rotated for ${CLIENT_ID}" | \
      slack-notify --channel "#security"
    # Rotate quarterly
    0 0 1 */3 * /path/to/rotate-secrets.sh
    CLIENT_SECRET_LIFETIME_DAYS=90
    GET /api/clients?secret_expires_before=2024-04-01
    Authorization: Bearer {admin_token}
    GET /api/audit-logs?event=client.secret_rotated&client_id={client_id}
    Authorization: Bearer {admin_token}
    # Notify on rotation
    NOTIFY_ON_SECRET_ROTATION=true
    NOTIFICATION_WEBHOOK=https://hooks.slack.com/...
    # Revoke all tokens for client
    POST /api/clients/{client_id}/revoke_tokens
    Authorization: Bearer {admin_token}
    GET /api/users/{id}/sessions
    Authorization: Bearer {admin_token}
    {
      "data": [
        {
          "id": "session-uuid",
          "created_at": "2024-01-15T10:30:00Z",
          "last_activity": "2024-01-15T14:20:00Z",
          "ip_address": "192.168.1.100",
          "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
          "device": "Chrome on macOS",
          "location": "San Francisco, CA"
        }
      ]
    }
    DELETE /api/sessions/{session_id}
    Authorization: Bearer {admin_token}
    DELETE /api/users/{id}/sessions
    Authorization: Bearer {admin_token}
    SINGLE_SESSION=true
    IDLE_TIMEOUT_MINUTES=30
    NOTIFY_NEW_SESSION=true
    New login to your Authority account
    
    Device: Chrome on macOS
    Location: San Francisco, CA
    Time: January 15, 2024 at 10:30 AM
    
    If this wasn't you, secure your account immediately.
    crystal run src/tasks/revoke_all_sessions.cr
    # Revoke sessions older than 30 days
    DELETE /api/sessions?older_than=30d
    Authorization: Bearer {admin_token}
    
    # Revoke sessions from specific IP
    DELETE /api/sessions?ip=192.168.1.100
    Authorization: Bearer {admin_token}
    User Session
        └── Access Token 1
        └── Access Token 2
        └── Refresh Token 1
    REVOKE_TOKENS_ON_SESSION_END=true
    GET /api/metrics/sessions
    Authorization: Bearer {admin_token}
    {
      "active_sessions": 1250,
      "sessions_today": 342,
      "unique_users": 890
    }
    GET /authorize?response_type=code&client_id=abc123&redirect_uri=https://app.example.com/callback&scope=openid%20profile&state=xyz789
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "scope": "openid profile email"
    }
    {
      "active": true,
      "client_id": "abc123",
      "username": "user@example.com",
      "scope": "openid profile email",
      "sub": "user-uuid",
      "token_type": "Bearer",
      "exp": 1699999999,
      "iat": 1699996399
    }
    {
      "active": false
    }
    {
      "device_code": "e2623df1-8594-47b4-b528-41ed3daecc1a",
      "user_code": "56933A",
      "verification_uri": "https://auth.example.com/activate",
      "verification_uri_complete": "https://auth.example.com/activate?user_code=56933A",
      "audience": "My Application",
      "expires_in": 300,
      "interval": 5
    }
    {
      "error": "authorization_pending",
      "error_description": "The user has not yet completed authorization"
    }
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "...",
      "scope": "openid profile"
    }
    {
      "sub": "user-uuid",
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "email": "john@example.com",
      "email_verified": true
    }
    {
      "issuer": "https://auth.example.com",
      "authorization_endpoint": "https://auth.example.com/authorize",
      "token_endpoint": "https://auth.example.com/token",
      "userinfo_endpoint": "https://auth.example.com/oauth2/userinfo",
      "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
      "introspection_endpoint": "https://auth.example.com/oauth/introspect",
      "revocation_endpoint": "https://auth.example.com/oauth/revoke",
      "device_authorization_endpoint": "https://auth.example.com/device/code",
      "scopes_supported": ["openid", "profile", "email", "read", "write"],
      "response_types_supported": ["code", "token"],
      "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "password",
        "refresh_token",
        "urn:ietf:params:oauth:grant-type:device_code"
      ],
      "subject_types_supported": ["public"],
      "id_token_signing_alg_values_supported": ["RS256"],
      "code_challenge_methods_supported": ["S256", "plain"],
      "claims_supported": [
        "sub", "iss", "aud", "exp", "iat",
        "email", "email_verified", "name",
        "given_name", "family_name"
      ]
    }
    {
      "keys": [
        {
          "kty": "RSA",
          "kid": "authority-key-1",
          "use": "sig",
          "alg": "RS256",
          "n": "...",
          "e": "AQAB"
        }
      ]
    }
    {
      "client_name": "My Application",
      "redirect_uris": ["https://app.example.com/callback"],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "token_endpoint_auth_method": "client_secret_basic",
      "scope": "openid profile email",
      "logo_uri": "https://app.example.com/logo.png",
      "client_uri": "https://app.example.com",
      "tos_uri": "https://app.example.com/tos",
      "policy_uri": "https://app.example.com/privacy",
      "contacts": ["admin@example.com"]
    }
    {
      "client_id": "abc123-uuid",
      "client_secret": "xyz789-secret",
      "client_id_issued_at": 1699996399,
      "client_secret_expires_at": 0,
      "client_name": "My Application",
      "redirect_uris": ["https://app.example.com/callback"],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "token_endpoint_auth_method": "client_secret_basic",
      "scope": "openid profile email"
    }
    {
      "success": true
    }
    {
      "error": "invalid_token"
    }
    {
      "status": "ok"
    }
    POST /register
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "client_name": "My Application",
      "redirect_uris": [
        "https://myapp.com/callback",
        "https://myapp.com/auth/callback"
      ],
      "grant_types": [
        "authorization_code",
        "refresh_token"
      ],
      "response_types": ["code"],
      "scope": "openid profile email",
      "token_endpoint_auth_method": "client_secret_basic"
    }
    {
      "client_id": "abc123def456",
      "client_secret": "xyz789ghi012",
      "client_name": "My Application",
      "redirect_uris": [
        "https://myapp.com/callback",
        "https://myapp.com/auth/callback"
      ],
      "grant_types": ["authorization_code", "refresh_token"],
      "response_types": ["code"],
      "scope": "openid profile email",
      "token_endpoint_auth_method": "client_secret_basic",
      "client_id_issued_at": 1705312200,
      "client_secret_expires_at": 0
    }
    GET /register/{client_id}
    Authorization: Bearer {admin_token}
    PATCH /register/{client_id}
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "redirect_uris": [
        "https://myapp.com/callback",
        "https://myapp.com/auth/callback",
        "https://staging.myapp.com/callback"
      ]
    }
    DELETE /register/{client_id}
    Authorization: Bearer {admin_token}
    https://myapp.com/callback
    https://myapp.com/auth/callback
    https://staging.myapp.com/callback
    http://localhost:3000/callback (development only)
    https://myapp.com/*          # No wildcards
    https://*.myapp.com/callback # No wildcards
    http://myapp.com/callback    # No HTTP in production
    {
      "scope": "openid profile email read write admin"
    }
    {
      "client_name": "My Application",
      "client_uri": "https://myapp.com",
      "logo_uri": "https://myapp.com/logo.png",
      "tos_uri": "https://myapp.com/terms",
      "policy_uri": "https://myapp.com/privacy",
      "contacts": ["support@myapp.com"]
    }
    # Open in browser
    open "http://localhost:4000/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&scope=openid"
    public/images/logo.png
    public/images/logo-dark.png  # For dark backgrounds
    public/images/favicon.ico
    <img src="/images/logo.png" alt="{{ app_name }}" class="logo">
    :root {
      /* Brand colors */
      --primary-color: #7c3aed;      /* Primary purple */
      --primary-hover: #6d28d9;      /* Darker purple */
      --primary-light: #a78bfa;      /* Lighter purple */
    
      /* Accent colors */
      --accent-color: #06b6d4;       /* Cyan accent */
    
      /* Status colors */
      --success: #22c55e;
      --warning: #f59e0b;
      --error: #ef4444;
      --info: #3b82f6;
    }
    :root {
      --bg-primary: #0f172a;
      --bg-secondary: #1e293b;
      --bg-card: #1e293b;
      --text-primary: #f8fafc;
      --text-secondary: #94a3b8;
      --border-color: #334155;
    }
    .theme-light {
      --bg-primary: #ffffff;
      --bg-secondary: #f8fafc;
      --bg-card: #ffffff;
      --text-primary: #0f172a;
      --text-secondary: #64748b;
      --border-color: #e2e8f0;
    }
    APP_NAME=MyAuth
    APP_TAGLINE=Secure authentication for everyone
    <title>{{ app_name }} - Sign In</title>
    <p>{{ app_tagline }}</p>
    <!-- In layout.html -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    :root {
      --font-family: 'Inter', system-ui, sans-serif;
    }
    
    body {
      font-family: var(--font-family);
    }
    @font-face {
      font-family: 'CustomFont';
      src: url('/fonts/custom.woff2') format('woff2');
      font-weight: normal;
      font-style: normal;
    }
    .login-page {
      background-image: url('/images/login-bg.jpg');
      background-size: cover;
      background-position: center;
    }
    .login-card {
      background: var(--bg-card);
      border-radius: var(--radius-lg);
      box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
      padding: 2rem;
    }
    <!-- emails/base.html -->
    <div class="header" style="background-color: #7c3aed; padding: 20px; text-align: center;">
      <img src="{{ base_url }}/images/logo-dark.png" alt="{{ app_name }}" style="max-width: 150px;">
    </div>
    <div class="footer" style="text-align: center; padding: 20px; color: #666;">
      <p>&copy; {{ year }} {{ company_name }}. All rights reserved.</p>
      <p>{{ company_address }}</p>
    </div>
    <div class="client-info">
      {% if client.logo_uri %}
      <img src="{{ client.logo_uri }}" alt="{{ client.name }}" class="client-logo">
      {% endif %}
      <h2>{{ client.name }}</h2>
    </div>
    <ul class="scopes">
      {% for scope in scopes %}
      <li>
        <i class="icon icon-{{ scope.name }}"></i>
        {{ scope.description }}
      </li>
      {% endfor %}
    </ul>
    # Branding
    APP_NAME=MyAuth
    APP_TAGLINE=Secure authentication
    COMPANY_NAME=My Company
    COMPANY_ADDRESS=123 Main St, City
    
    # URLs
    LOGO_URL=/images/logo.png
    FAVICON_URL=/images/favicon.ico
    TERMS_URL=https://example.com/terms
    PRIVACY_URL=https://example.com/privacy
    SUPPORT_URL=https://example.com/support
    
    # Theme
    THEME=dark  # dark or light
    PRIMARY_COLOR=#7c3aed
    public/templates/emails/
    ├── base.html           # Base layout
    ├── welcome.html        # Welcome email
    ├── verification.html   # Email verification
    ├── password-reset.html # Password reset
    ├── mfa-enabled.html    # MFA confirmation
    └── new-login.html      # New login alert
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <style>
        body {
          font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
          line-height: 1.6;
          color: #333;
          max-width: 600px;
          margin: 0 auto;
          padding: 20px;
        }
        .header {
          text-align: center;
          margin-bottom: 30px;
        }
        .logo {
          max-width: 150px;
        }
        .button {
          display: inline-block;
          padding: 12px 24px;
          background-color: #7c3aed;
          color: white;
          text-decoration: none;
          border-radius: 6px;
          margin: 20px 0;
        }
        .footer {
          margin-top: 40px;
          padding-top: 20px;
          border-top: 1px solid #eee;
          font-size: 12px;
          color: #666;
        }
      </style>
    </head>
    <body>
      <div class="header">
        <img src="{{ base_url }}/images/logo.png" alt="Authority" class="logo">
      </div>
    
      {% block content %}{% endblock %}
    
      <div class="footer">
        <p>This email was sent by {{ app_name }}.</p>
        <p>{{ company_address }}</p>
      </div>
    </body>
    </html>
    {% extends "emails/base.html" %}
    
    {% block content %}
    <h1>Reset Your Password</h1>
    
    <p>Hi {{ user.name }},</p>
    
    <p>We received a request to reset your password. Click the button below to choose a new password:</p>
    
    <p style="text-align: center;">
      <a href="{{ reset_url }}" class="button">Reset Password</a>
    </p>
    
    <p>This link will expire in {{ expiry_minutes }} minutes.</p>
    
    <p>If you didn't request a password reset, you can safely ignore this email. Your password won't be changed.</p>
    
    <p>
      Best regards,<br>
      The {{ app_name }} Team
    </p>
    {% endblock %}
    {% extends "emails/base.html" %}
    
    {% block content %}
    <h1>Welcome to {{ app_name }}!</h1>
    
    <p>Hi {{ user.name }},</p>
    
    <p>Thanks for creating an account. We're excited to have you on board!</p>
    
    <p>Here are a few things you can do:</p>
    
    <ul>
      <li>Complete your profile</li>
      <li>Enable two-factor authentication</li>
      <li>Connect your applications</li>
    </ul>
    
    <p style="text-align: center;">
      <a href="{{ profile_url }}" class="button">View Your Profile</a>
    </p>
    
    <p>
      Welcome aboard,<br>
      The {{ app_name }} Team
    </p>
    {% endblock %}
    SMTP_HOST=smtp.example.com
    SMTP_PORT=587
    SMTP_USER=notifications@example.com
    SMTP_PASSWORD=your_password
    SMTP_FROM=noreply@example.com
    SMTP_FROM_NAME=Authority
    EMAIL_VERIFICATION_REQUIRED=true
    EMAIL_VERIFICATION_TTL=86400
    PASSWORD_RESET_TTL=3600
    # Development mode shows emails in browser
    CRYSTAL_ENV=development
    EMAIL_PREVIEW=true
    crystal run src/tasks/send_test_email.cr -- \
      --to test@example.com \
      --template password-reset
    <!-- Use inline styles -->
    <p style="color: #333; font-size: 16px;">Text</p>
    
    <!-- Use tables for layout -->
    <table width="100%" cellpadding="0" cellspacing="0">
      <tr>
        <td>Content</td>
      </tr>
    </table>
    
    <!-- Simple buttons -->
    <a href="{{ url }}" style="
      display: inline-block;
      padding: 12px 24px;
      background-color: #7c3aed;
      color: #ffffff;
      text-decoration: none;
    ">Button</a>
    public/templates/emails/
    ├── en/
    │   ├── welcome.html
    │   └── password-reset.html
    ├── es/
    │   ├── welcome.html
    │   └── password-reset.html
    └── fr/
        ├── welcome.html
        └── password-reset.html
    DEFAULT_LOCALE=en
  • Maintain accessibility (ARIA, contrast)

  • Preserve security features (CSRF tokens)

  • Disabling client-side validation

  • Adding inline JavaScript

  • forward_url

    Redirect URL after login

    error

    Error message

    username

    Previously entered username

    client_name

    OAuth client name

    client_id

    Client identifier

    scopes

    Requested scopes

    redirect_uri

    Callback URL

    state

    CSRF state parameter

    user_code

    Pre-filled code

    audience

    Client name

    error

    Error message

    Email Templates
    Branding
    User Interface Referencearrow-up-right
    Landing Page
    Sign In
    Admin Dashboard

    state

    Recommended

    CSRF protection token

    Validate redirect_uri - Use exact match
  • Short-lived codes - Authorization codes expire quickly

  • response_type

    Yes

    Must be code

    client_id

    Yes

    Your client identifier

    redirect_uri

    Yes

    URL to redirect after authorization

    scope

    Yes

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    Must be authorization_code

    code

    Yes

    The authorization code

    redirect_uri

    Yes

    Same as authorization request

    Authorization Code + PKCE
    Refresh Tokens
    Token Response

    Space-separated scopes

    OAuth 2.0 Errors

    hashtag
    Authorization Endpoint Errors

    Error
    Description

    invalid_request

    Missing or invalid parameter

    unauthorized_client

    Client not authorized for this grant type

    access_denied

    User denied authorization

    unsupported_response_type

    Response type not supported

    invalid_scope

    Invalid or unknown scope

    server_error

    Server encountered an error

    Example:

    hashtag
    Token Endpoint Errors

    Error
    HTTP Status
    Description

    invalid_request

    400

    Missing required parameter

    invalid_client

    401

    Client authentication failed

    invalid_grant

    400

    Invalid authorization code or refresh token

    unauthorized_client

    400

    Example:

    hashtag
    Token Introspection Errors

    Error
    HTTP Status
    Description

    invalid_request

    400

    Missing token parameter

    invalid_client

    401

    Client authentication failed

    hashtag
    Device Flow Errors

    Error
    HTTP Status
    Description

    authorization_pending

    400

    User hasn't completed authorization

    slow_down

    400

    Polling too frequently

    access_denied

    401

    User denied authorization

    expired_token

    400

    Example:

    hashtag
    OpenID Connect Errors

    hashtag
    UserInfo Errors

    Error
    HTTP Status
    Description

    invalid_token

    401

    Token is invalid or expired

    insufficient_scope

    403

    Token lacks required scope

    Example:

    hashtag
    HTTP Status Codes

    Status
    Meaning

    200

    Success

    201

    Created

    302

    Redirect

    400

    Bad Request - Invalid parameters

    401

    Unauthorized - Authentication required

    403

    Forbidden - Insufficient permissions

    hashtag
    API Errors

    hashtag
    Authentication Errors

    Error
    HTTP Status
    Description

    authentication_required

    401

    No valid authentication provided

    invalid_credentials

    401

    Username or password incorrect

    account_locked

    403

    Account is locked

    mfa_required

    403

    hashtag
    Validation Errors

    Error
    HTTP Status
    Description

    validation_error

    400

    Request validation failed

    missing_parameter

    400

    Required parameter missing

    invalid_parameter

    400

    Parameter format invalid

    Example with Details:

    hashtag
    Resource Errors

    Error
    HTTP Status
    Description

    not_found

    404

    Resource not found

    already_exists

    409

    Resource already exists

    gone

    410

    Resource no longer available

    hashtag
    Rate Limiting Errors

    Error
    HTTP Status
    Description

    rate_limit_exceeded

    429

    Too many requests

    Example:

    hashtag
    Troubleshooting

    hashtag
    Common Issues

    Error
    Cause
    Solution

    invalid_client

    Wrong client credentials

    Verify client_id and client_secret

    invalid_grant

    Expired or used code

    Request new authorization code

    invalid_redirect_uri

    Mismatched redirect URI

    Use exact registered URI

    invalid_scope

    Unknown scope

    Use only registered scopes

    hashtag
    Debug Tips

    1. Check the error_description for details

    2. Verify all required parameters are included

    3. Ensure client credentials are correct

    4. Check token expiration

    5. Review server logs for more context

    hashtag
    Next Steps

    • Rate Limits - Rate limiting details

    • API Endpoints - Endpoint reference

    • OAuth 2.0 Flows - Grant specifications

    hashtag
    When to Use
    • Mobile applications

    • Single-page applications (SPAs)

    • Desktop applications

    • Any client that cannot securely store secrets

    hashtag
    Flow Diagram

    hashtag
    PKCE Parameters

    Parameter
    Description

    code_verifier

    Random string 43-128 characters

    code_challenge

    Base64URL(SHA256(code_verifier))

    code_challenge_method

    S256 (recommended) or plain

    hashtag
    Generate PKCE Values

    hashtag
    JavaScript

    hashtag
    Python

    hashtag
    Authorization Request

    GET /authorize

    hashtag
    Parameters

    Parameter
    Required
    Description

    response_type

    Yes

    Must be code

    client_id

    Yes

    Your client identifier

    redirect_uri

    Yes

    Callback URL

    scope

    Yes

    hashtag
    Example

    hashtag
    Token Request

    POST /token

    hashtag
    Parameters

    Parameter
    Required
    Description

    grant_type

    Yes

    authorization_code

    code

    Yes

    Authorization code

    redirect_uri

    Yes

    Same as authorization

    client_id

    Yes

    hashtag
    Example

    circle-info

    Note: No client_secret is required for public clients with PKCE.

    hashtag
    Complete Example

    hashtag
    React SPA

    hashtag
    Mobile (React Native)

    hashtag
    Security Benefits

    PKCE prevents:

    1. Authorization code interception - Attacker cannot use stolen code without verifier

    2. Redirect hijacking - Code is useless without the original verifier

    3. Man-in-the-middle attacks - Verifier is never transmitted before token exchange

    hashtag
    Best Practices

    circle-check
    • Always use S256 method (not plain)

    • Generate cryptographically random verifiers

    • Store verifier securely during flow

    • Clear verifier after token exchange

    hashtag
    Next Steps

    • Authorization Code - Confidential clients

    • Refresh Tokens - Token renewal

    • Token Response - Response format

    Token Lifecycle

    hashtag
    Token Response

    When you receive tokens, note the expiration:

    Field
    Description

    access_token

    Short-lived token for API access

    refresh_token

    Long-lived token for renewal

    expires_in

    Access token lifetime in seconds

    hashtag
    Refresh Token Request

    POST /token

    hashtag
    Headers

    Header
    Value

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    hashtag
    Parameters

    Parameter
    Required
    Description

    grant_type

    Yes

    Must be refresh_token

    refresh_token

    Yes

    The refresh token

    scope

    Optional

    Request subset of original scopes

    hashtag
    Example

    hashtag
    Response

    circle-info

    Authority rotates refresh tokens by default. The old refresh token is invalidated when a new one is issued.

    hashtag
    Token Rotation

    With rotation enabled, each refresh:

    1. Issues a new access token

    2. Issues a new refresh token

    3. Invalidates the old refresh token

    This limits damage if a refresh token is compromised.

    hashtag
    Implementation

    hashtag
    Proactive Refresh

    Refresh before expiration:

    hashtag
    Reactive Refresh

    Refresh when access token is rejected:

    hashtag
    Error Handling

    hashtag
    Invalid Refresh Token

    When this happens, the user must re-authenticate.

    hashtag
    Handling Errors

    hashtag
    Scope Reduction

    Request fewer scopes when refreshing:

    You cannot request more scopes than originally granted.

    hashtag
    Configuration

    hashtag
    Token Lifetimes

    Variable
    Default
    Description

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    REFRESH_TOKEN_TTL

    2592000

    Refresh token lifetime (30 days)

    REFRESH_TOKEN_ROTATION

    true

    Rotate refresh tokens

    hashtag
    Offline Access

    To receive refresh tokens, request the offline_access scope:

    hashtag
    Security Best Practices

    circle-check

    Do:

    • Store refresh tokens securely

    • Use token rotation

    • Refresh proactively before expiry

    • Handle refresh failures gracefully

    circle-exclamation

    Avoid:

    • Storing refresh tokens in localStorage

    • Sharing refresh tokens between tabs

    • Ignoring rotation (using old tokens)

    • Long refresh token lifetimes without rotation

    hashtag
    Revocation

    Revoke refresh tokens when:

    • User logs out

    • User changes password

    • Security concern detected

    hashtag
    Next Steps

    • Token Response - Response format

    • Error Codes - Error handling

    • Token Lifecycle - Conceptual overview

    hashtag
    Grant Type Comparison
    Grant Type
    Use Case
    Client Type
    Security

    Web apps with backend

    Confidential

    High

    Mobile apps, SPAs

    Public

    High

    Server-to-server

    Confidential

    hashtag
    Endpoints

    Endpoint
    Method
    Description

    /authorize

    GET

    Start authorization flow

    /token

    POST

    Exchange code for tokens

    /token/introspect

    POST

    Validate a token

    /token/revoke

    POST

    Revoke a token

    hashtag
    Recommended Flows

    hashtag
    For Web Applications

    Use Authorization Code with a backend:

    hashtag
    For Mobile Apps / SPAs

    Use Authorization Code + PKCE:

    hashtag
    For Backend Services

    Use Client Credentials:

    hashtag
    For CLI / IoT Devices

    Use Device Code:

    hashtag
    Security Recommendations

    circle-check

    Do:

    • Always use HTTPS

    • Validate state parameter

    • Use PKCE for public clients

    • Store tokens securely

    • Validate tokens server-side

    circle-exclamation

    Avoid:

    • Implicit grant (deprecated)

    • Password grant for third-party apps

    • Long-lived access tokens

    • Storing tokens in localStorage

    hashtag
    Standards

    Authority implements these RFCs:

    • RFC 6749arrow-up-right - OAuth 2.0 Framework

    • RFC 6750arrow-up-right - Bearer Token Usage

    • RFC 7636arrow-up-right - PKCE

    • - Token Introspection

    • - Token Revocation

    • - Device Authorization

    hashtag
    Next Steps

    • Authorization Code - Standard web app flow

    • Authorization Code + PKCE - Mobile/SPA flow

    • Client Credentials - Machine-to-machine

    • - IoT and CLI devices

    OpenID Connect

    OpenID Connect (OIDC) adds identity layer on top of OAuth 2.0, providing user authentication.

    hashtag
    Overview

    While OAuth 2.0 provides authorization, OpenID Connect adds:

    • Authentication - Verify user identity

    • ID Tokens - User information in JWT format

    • UserInfo Endpoint - Fetch additional user claims

    • Discovery - Automatic configuration

    hashtag
    Endpoints

    Endpoint
    Description

    hashtag
    Scopes

    Request OIDC scopes to get identity information:

    Scope
    Claims

    hashtag
    Flow

    hashtag
    ID Token

    The ID token is a JWT containing user identity:

    See for full specification.

    hashtag
    UserInfo Response

    See for details.

    hashtag
    Discovery

    Automatic configuration via:

    Response includes all endpoints, supported scopes, algorithms, etc.

    See for full specification.

    hashtag
    Authentication vs Authorization

    Aspect
    OAuth 2.0
    OpenID Connect

    hashtag
    Basic Implementation

    hashtag
    Standards

    Authority implements:

    hashtag
    Next Steps

    • - Automatic configuration

    • - Token verification keys

    • - User claims endpoint

    Password Reset

    Configure and manage password reset flows in Authority.

    hashtag
    User Self-Service Reset

    hashtag
    Reset Flow

    1. User clicks Forgot Password on login page

    2. Enters email address

    3. Receives reset link via email

    4. Clicks link and sets new password

    hashtag
    Request Reset

    Users visit /forgot-password and enter their email.

    hashtag
    Reset Email

    Authority sends an email with a secure reset link:

    hashtag
    Configuration

    hashtag
    Environment Variables

    Variable
    Default
    Description

    hashtag
    Email Template

    Customize the reset email in public/templates/emails/password-reset.html:

    hashtag
    Admin Password Reset

    hashtag
    Reset via Dashboard

    1. Navigate to Admin Dashboard → Users

    2. Select the user

    3. Click Reset Password

    hashtag
    Reset via API

    hashtag
    Send Reset Email

    hashtag
    Set Password Directly

    circle-exclamation

    Setting passwords directly should notify the user.

    hashtag
    Security Considerations

    hashtag
    Rate Limiting

    Prevent abuse of reset endpoint:

    This allows 3 reset requests per hour per email.

    hashtag
    Token Security

    Reset tokens are:

    • Single-use

    • Time-limited

    • Cryptographically random

    • Invalidated on password change

    hashtag
    Email Enumeration Protection

    Don't reveal if email exists:

    Response is always:

    hashtag
    Audit Trail

    Password reset events are logged:

    Event
    Description

    hashtag
    Customization

    hashtag
    Reset Page Styling

    Customize public/templates/reset-password.html:

    hashtag
    Success Page

    After successful reset, show:

    hashtag
    Troubleshooting

    hashtag
    Email Not Received

    • Check spam folder

    • Verify email configuration

    • Check logs for sending errors

    • Confirm email address is correct

    hashtag
    Token Expired

    • Request new reset link

    • Check PASSWORD_RESET_TTL setting

    hashtag
    Password Not Accepted

    • Verify password meets policy requirements

    • Check password history restrictions

    hashtag
    Integration

    hashtag
    Custom Reset Flow

    For custom applications:

    hashtag
    Next Steps

    • - Admin accounts

    • - Session management

    • - Password requirements

    UserInfo

    The UserInfo endpoint returns claims about the authenticated user.

    hashtag
    Endpoint

    GET /userinfo POST /userinfo

    Both methods are supported.

    hashtag
    Request

    hashtag
    Authorization Header

    hashtag
    POST with Form Body

    hashtag
    Response

    hashtag
    Claims by Scope

    Claims returned depend on requested scopes:

    hashtag
    openid (required)

    Claim
    Type
    Description

    hashtag
    profile

    Claim
    Type
    Description

    hashtag
    email

    Claim
    Type
    Description

    hashtag
    address

    Claim
    Type
    Description

    Address object:

    hashtag
    phone

    Claim
    Type
    Description

    hashtag
    Usage

    hashtag
    JavaScript

    hashtag
    Python

    hashtag
    Error Responses

    hashtag
    Invalid Token

    hashtag
    Expired Token

    hashtag
    Insufficient Scope

    hashtag
    UserInfo vs ID Token

    Aspect
    ID Token
    UserInfo

    Use ID token for authentication, UserInfo for current profile data.

    hashtag
    Caching

    UserInfo responses can be cached briefly:

    hashtag
    Next Steps

    • - Token claims

    • - Token verification

    • - Endpoint discovery

    Understanding Authority

    This section explains the concepts, architecture, and design decisions behind Authority.

    hashtag
    Understanding OAuth 2.0

    • OAuth 2.0 Concepts - Fundamentals of the OAuth 2.0 protocol

    • - Identity layer on top of OAuth 2.0

    • - Decision guide for selecting the right flow

    hashtag
    Architecture

    • - How Authority is built and organized

    • - Security architecture and design principles

    • - How tokens are issued, used, and revoked

    hashtag
    About Authority

    • - Benefits and use cases

    hashtag
    How to Use This Section

    Unlike tutorials (which teach by doing) and how-to guides (which solve specific problems), explanations provide understanding. Read these when you want to:

    • Understand why things work the way they do

    • Learn the theory behind the implementation

    • Make informed architectural decisions

    hashtag
    Further Reading

    After understanding the concepts, explore:

    • - Learn by building

    • - Solve specific problems

    • - Technical specifications

    Discovery

    OpenID Connect Discovery allows clients to automatically configure themselves.

    hashtag
    Discovery Endpoint

    GET /.well-known/openid-configuration

    Returns a JSON document with all provider configuration.

    hashtag
    Response

    hashtag
    Fields

    hashtag
    Core Endpoints

    Field
    Description

    hashtag
    Additional Endpoints

    Field
    Description

    hashtag
    Supported Features

    Field
    Description

    hashtag
    Usage

    hashtag
    JavaScript

    hashtag
    Python

    hashtag
    Caching

    Discovery documents should be cached:

    hashtag
    Validation

    Clients should verify:

    1. issuer matches the expected provider

    2. Required endpoints are present

    3. Needed scopes/grant types are supported

    hashtag
    Next Steps

    • - Public keys for verification

    • - User claims endpoint

    • - Token specification

    Configuration

    Complete reference for all Authority configuration options.

    hashtag
    Server

    Variable
    Default
    Description

    CRYSTAL_ENV

    hashtag
    Database

    Variable
    Default
    Description

    hashtag
    Security

    Variable
    Default
    Description

    See for security-specific options.

    hashtag
    Tokens

    Variable
    Default
    Description

    See for token-specific options.

    hashtag
    Sessions

    Variable
    Default
    Description

    hashtag
    SSL/TLS

    Variable
    Default
    Description

    hashtag
    Templates

    Variable
    Default
    Description

    hashtag
    Logging

    Variable
    Default
    Description

    hashtag
    Email

    Variable
    Default
    Description

    hashtag
    Redis

    Variable
    Default
    Description

    hashtag
    Rate Limiting

    Variable
    Default
    Description

    hashtag
    Branding

    Variable
    Default
    Description

    hashtag
    OAuth

    Variable
    Default
    Description

    hashtag
    Example Configuration

    hashtag
    Development

    hashtag
    Production

    hashtag
    Next Steps

    • - Security configuration

    • - Token configuration

    • - Setup guide

    Token Settings

    Configuration options for OAuth 2.0 tokens.

    hashtag
    Token Lifetimes

    Variable
    Default
    Description

    ACCESS_TOKEN_TTL

    hashtag
    Refresh Token Settings

    Variable
    Default
    Description

    hashtag
    Token Format

    Variable
    Default
    Description

    hashtag
    Client-Specific Overrides

    Configure per-client token settings via API:

    hashtag
    Token Claims

    hashtag
    Access Token Claims

    Claim
    Description

    hashtag
    ID Token Claims

    Claim
    Description

    hashtag
    Introspection Settings

    Variable
    Default
    Description

    hashtag
    Revocation Settings

    Variable
    Default
    Description

    hashtag
    Example Configurations

    hashtag
    Short-Lived Tokens (High Security)

    hashtag
    Long-Lived Tokens (User Convenience)

    hashtag
    API-Only (No Refresh)

    hashtag
    Token Lifecycle

    hashtag
    Best Practices

    hashtag
    Access Tokens

    • Keep short-lived (15 min - 1 hour)

    • Use for API authorization only

    • Validate on each request

    hashtag
    Refresh Tokens

    • Enable rotation

    • Set reasonable lifetime

    • Revoke on security events

    hashtag
    Authorization Codes

    • Very short-lived (5-10 minutes)

    • Single use only

    • Bind to client

    hashtag
    Next Steps

    • - Security configuration

    • - Complete reference

    • - Conceptual overview

    Legacy Flows

    triangle-exclamation

    Deprecated: The implicit grant is no longer recommended for new applications. Use Authorization Code + PKCE instead.

    hashtag
    Overview

    The implicit grant returns tokens directly in the URL fragment. It was designed for browser-based applications before PKCE existed.

    hashtag
    Why It's Deprecated

    • Tokens exposed in URL - Visible in browser history and logs

    • No refresh tokens - Users must re-authenticate frequently

    • Vulnerable to interception - No protection against token leakage

    hashtag
    Flow

    hashtag
    Authorization Request

    hashtag
    Response

    Note: Token is in URL fragment (#), not query string.

    hashtag
    Migration to PKCE

    Replace implicit grant with authorization code + PKCE:

    Before (Implicit):

    After (PKCE):

    hashtag
    Next Steps

    • - Recommended replacement

    • - All grant types

    Password Grant

    circle-exclamation

    Not Recommended: The password grant should only be used for first-party, trusted applications. Never allow third-party apps to use this grant.

    hashtag
    Overview

    The password grant allows applications to exchange user credentials directly for tokens. This bypasses the normal authorization flow.

    hashtag
    When to Use

    Acceptable:

    • First-party mobile apps (owned by same company)

    • Migration from legacy systems

    • Trusted internal tools

    Never use for:

    • Third-party applications

    • Public-facing apps

    • Any app you don't fully trust

    hashtag
    Token Request

    POST /token

    hashtag
    Parameters

    Parameter
    Required
    Description

    hashtag
    Example

    hashtag
    Response

    hashtag
    Security Risks

    1. Credential exposure - App sees user's password

    2. No consent - User doesn't explicitly authorize scopes

    3. Phishing risk - Encourages entering passwords in apps

    hashtag
    Configuration

    To enable password grant (not recommended):

    hashtag
    Migration Path

    Replace password grant with proper OAuth flows:

    hashtag
    Mobile Apps

    Use Authorization Code + PKCE:

    hashtag
    Web Applications

    Use Authorization Code:

    hashtag
    Next Steps

    • - Recommended for mobile/SPA

    • - Recommended for web apps

    • - For machine-to-machine

    Glossary

    OAuth 2.0 and OpenID Connect terminology.

    hashtag
    Core Concepts

    hashtag
    Resource Owner

    The entity capable of granting access to a protected resource. When the resource owner is a person, it is referred to as an end-user.

    hashtag
    Resource Server

    The server hosting the protected resources. Also known as the API server. It accepts and responds to protected resource requests using access tokens.

    hashtag
    Client

    An application making protected resource requests on behalf of the resource owner. The term "client" does not imply any particular implementation (server, desktop, mobile, etc.).

    hashtag
    Authorization Server

    The server that issues access tokens to the client after authenticating the resource owner and obtaining authorization. Authority is an authorization server.

    hashtag
    Token Types

    hashtag
    Access Token

    A credential used to access protected resources. Access tokens represent the authorization granted to the client. Authority issues JWTs as access tokens.

    hashtag
    Refresh Token

    A credential used to obtain new access tokens. Refresh tokens are issued with access tokens and allow clients to get new tokens without user interaction.

    hashtag
    ID Token

    A JSON Web Token (JWT) that contains claims about the authentication event and the user. Part of OpenID Connect.

    hashtag
    Authorization Code

    A short-lived code returned after user authorization. Exchanged for access tokens at the token endpoint.

    hashtag
    Device Code

    A code used in the device authorization flow. Displayed to users who authorize on a separate device.

    hashtag
    OAuth Concepts

    hashtag
    Grant Type

    The method used to obtain an access token. Common types:

    • Authorization Code - Web applications

    • Client Credentials - Machine-to-machine

    • Device Code - IoT devices, CLIs

    hashtag
    Scope

    A string that defines the access level requested by the client. Examples: read, write, openid, profile.

    hashtag
    Redirect URI

    The URL where the authorization server redirects after authorization. Also called callback URL.

    hashtag
    State

    A random value used to prevent CSRF attacks during the authorization flow.

    hashtag
    PKCE (Proof Key for Code Exchange)

    An extension that protects authorization code flow against interception. Pronounced "pixie".

    hashtag
    Code Verifier

    A random string used in PKCE. Created by the client before authorization.

    hashtag
    Code Challenge

    A transformation of the code verifier. Sent in the authorization request and verified during token exchange.

    hashtag
    Client Types

    hashtag
    Confidential Client

    A client capable of maintaining the confidentiality of its credentials. Typically server-side applications.

    hashtag
    Public Client

    A client that cannot maintain secret credentials. Mobile apps, SPAs, and native applications.

    hashtag
    First-Party Client

    A client owned by the same entity that operates the authorization server. Trusted with user credentials.

    hashtag
    Third-Party Client

    A client owned by a different entity. Should never directly handle user credentials.

    hashtag
    OpenID Connect

    hashtag
    Claims

    Pieces of information about a user. Examples: name, email, sub.

    hashtag
    UserInfo Endpoint

    An endpoint that returns claims about the authenticated user.

    hashtag
    Discovery

    The mechanism to automatically find authorization server endpoints and capabilities.

    hashtag
    JWKS (JSON Web Key Set)

    A set of public keys used to verify token signatures.

    hashtag
    Nonce

    A random value sent in the authorization request and included in the ID token to prevent replay attacks.

    hashtag
    Authentication

    hashtag
    MFA (Multi-Factor Authentication)

    Authentication requiring multiple verification methods. Authority supports TOTP-based MFA.

    hashtag
    TOTP (Time-based One-Time Password)

    A temporary passcode generated by an authenticator app. Changes every 30 seconds.

    hashtag
    Session

    A server-side record of a user's authentication state. Persists across requests.

    hashtag
    Security

    hashtag
    Token Introspection

    A mechanism for resource servers to query the authorization server about a token's current state.

    hashtag
    Token Revocation

    The process of invalidating a token before its natural expiration.

    hashtag
    Consent

    The user's explicit approval of the scopes requested by a client.

    hashtag
    Audit Log

    A record of security-relevant events for compliance and monitoring.

    hashtag
    Account Lockout

    Temporarily blocking access after multiple failed authentication attempts.

    hashtag
    JWT (JSON Web Token)

    hashtag
    Header

    The first part of a JWT containing the token type and signing algorithm.

    hashtag
    Payload

    The second part of a JWT containing claims about the subject.

    hashtag
    Signature

    The third part of a JWT used to verify the token hasn't been tampered with.

    hashtag
    Issuer (iss)

    The entity that issued the token. For Authority tokens, this is the server URL.

    hashtag
    Subject (sub)

    The identifier of the principal (user) the token is about.

    hashtag
    Audience (aud)

    The intended recipient of the token. Typically the client ID.

    hashtag
    Expiration (exp)

    The timestamp after which the token should not be accepted.

    hashtag
    Standards

    hashtag
    RFC 6749

    The OAuth 2.0 Authorization Framework specification.

    hashtag
    RFC 6750

    OAuth 2.0 Bearer Token Usage specification.

    hashtag
    RFC 7636

    Proof Key for Code Exchange (PKCE) specification.

    hashtag
    RFC 8628

    Device Authorization Grant specification.

    hashtag
    OpenID Connect Core 1.0

    The specification for identity layer on top of OAuth 2.0.

    Configure Facebook

    Enable users to sign in with their Facebook accounts.

    hashtag
    Prerequisites

    • Authority instance running

    • Admin access to Authority dashboard

    • Facebook Developer account

    hashtag
    Step 1: Create Facebook App

    1. Go to

    2. Click My Apps > Create App

    3. Select app type:

    hashtag
    Step 2: Configure Authority

    hashtag
    Using Admin Dashboard

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable Facebook OAuth

    hashtag
    Using Environment Variables

    hashtag
    Step 3: Add Login Button

    With forward URL:

    hashtag
    Step 4: App Review (Production)

    For production use, you need to:

    1. Complete Data Use Checkup - Explain how you use user data

    2. Add Privacy Policy URL - Required for public apps

    3. Submit for App Review - If requesting advanced permissions

    For basic login (email + public_profile), you may not need full review.

    hashtag
    User Data Retrieved

    Field
    Description

    hashtag
    Troubleshooting

    hashtag
    "App Not Active"

    Your Facebook app is in development mode.

    Solution:

    • In development mode, only app admins/developers/testers can log in

    • Add test users in Roles section

    • Or switch to Live mode after review

    hashtag
    "URL Blocked"

    Redirect URI isn't whitelisted.

    Solution: Add the exact callback URL to Valid OAuth Redirect URIs:

    hashtag
    No Email Retrieved

    User may have signed up with phone number or denied email permission.

    Solution: Handle missing email gracefully in your application.

    hashtag
    "Invalid Scopes"

    Requested scopes not approved for your app.

    Solution:

    • For email and public_profile, no review needed

    • Advanced scopes require App Review

    hashtag
    Privacy Considerations

    Facebook has strict data usage policies:

    • Only request data you need

    • Explain data usage in your privacy policy

    • Delete user data upon request

    hashtag
    Next Steps

    • - Add Google sign-in

    • - Account linking

    OAuth 2.0 Concepts

    Understanding the fundamentals of OAuth 2.0.

    hashtag
    What is OAuth 2.0?

    OAuth 2.0 is an authorization framework that enables applications to obtain limited access to user resources without exposing credentials.

    hashtag
    The Problem OAuth Solves

    Without OAuth, applications would need:

    • Direct access to user credentials

    • Full access to all resources

    • No way to revoke access without changing passwords

    OAuth provides:

    • Delegated authorization (no password sharing)

    • Scoped access (limited permissions)

    • Revocable tokens (easy access removal)

    hashtag
    Key Concepts

    hashtag
    Roles

    Role
    Description

    hashtag
    Tokens

    Token
    Purpose
    Lifetime

    hashtag
    Scopes

    Scopes define what access is granted:

    Clients request scopes:

    Users consent to scopes:

    hashtag
    Grant Types

    hashtag
    Authorization Code (Most Common)

    Best for: Web applications with a backend

    The client never sees the user's password.

    hashtag
    Authorization Code + PKCE

    Best for: Mobile apps, single-page applications

    Same as authorization code, but with proof key:

    1. Client generates random code_verifier

    2. Client sends code_challenge = SHA256(code_verifier)

    3. Authority returns code

    hashtag
    Client Credentials

    Best for: Machine-to-machine

    No user involved - the service itself is authorized.

    hashtag
    Device Code

    Best for: TVs, CLI tools, IoT

    hashtag
    The Authorization Flow

    hashtag
    Step by Step

    1. User wants to access protected resource

      User clicks "Login with Authority" in your app.

    2. Client redirects to Authorization Server

    3. User authenticates

    hashtag
    Security Concepts

    hashtag
    State Parameter

    Prevents CSRF attacks:

    1. Client generates random state

    2. Client stores state in session

    3. Client includes state in authorization request

    4. Authority returns state in callback

    hashtag
    PKCE

    Prevents authorization code interception:

    hashtag
    Token Security

    • Short-lived access tokens - Limit exposure window

    • Token rotation - New refresh token each use

    • Secure storage - Never store in URL or logs

    hashtag
    Common Misconceptions

    hashtag
    "OAuth is for authentication"

    OAuth is for authorization (what you can do), not authentication (who you are). OpenID Connect adds authentication.

    hashtag
    "The access token contains user data"

    Access tokens authorize access - they don't necessarily contain user info. Use the UserInfo endpoint or ID tokens for identity.

    hashtag
    "Longer token lifetimes are more secure"

    Shorter lifetimes with refresh tokens are more secure. If a token is compromised, the damage window is limited.

    hashtag
    Next Steps

    • - Identity layer

    • - Decision guide

    • - Token management

    OpenID Connect Concepts

    Understanding identity and authentication with OpenID Connect.

    hashtag
    OAuth vs OpenID Connect

    Aspect
    OAuth 2.0
    OpenID Connect

    Architecture

    Understanding how Authority is built and organized.

    hashtag
    System Overview

    hashtag
    Component Architecture

    ID Tokens

    ID tokens are JWTs that contain claims about the authenticated user.

    hashtag
    Overview

    ID tokens provide proof of authentication. Unlike access tokens (which authorize API access), ID tokens answer "who is this user?"

    hashtag

    Why Authority?

    Understanding the benefits and use cases for Authority.

    hashtag
    What is Authority?

    Authority is a production-ready OAuth 2.0 Server and OpenID Connect 1.0 Provider built with Crystal. It provides:

    Security Settings

    Configuration options for Authority security features.

    hashtag
    Account Lockout

    Variable
    Default
    Description

    Token Lifecycle

    Understanding how tokens are created, used, and retired in Authority.

    hashtag
    Token Types Overview

    hashtag
    Token States

    public/
    ├── templates/
    │   ├── layout.html          # Base layout
    │   ├── signin.html          # Login page
    │   ├── signup.html          # Registration
    │   ├── authorize.html       # OAuth consent
    │   ├── activate.html        # Device activation
    │   ├── forgot-password.html # Password reset request
    │   ├── reset-password.html  # Password reset form
    │   ├── errors.html          # Error messages
    │   └── emails/              # Email templates
    │       ├── verification.html
    │       ├── password-reset.html
    │       └── welcome.html
    ├── css/
    │   └── styles.css           # Main stylesheet
    └── js/
        └── app.js               # Client JavaScript
    <h1>Welcome, {{ user.name }}</h1>
    <p>Email: {{ user.email }}</p>
    {% if error %}
    <div class="alert alert-error">{{ error }}</div>
    {% endif %}
    
    {% if user.mfa_enabled %}
    <span class="badge">MFA Enabled</span>
    {% endif %}
    <ul class="scopes">
    {% for scope in scopes %}
      <li>{{ scope.name }}: {{ scope.description }}</li>
    {% endfor %}
    </ul>
    <!DOCTYPE html>
    <html>
    <head>
      <title>{% block title %}Authority{% endblock %}</title>
      <link rel="stylesheet" href="/css/styles.css">
    </head>
    <body>
      {% block body %}{% endblock %}
      <script src="/js/app.js"></script>
    </body>
    </html>
    {% extends "layout.html" %}
    {% set title = "Sign In" %}
    
    {% block body %}
    <main class="login-form">
      {% include "errors.html" %}
      <form action="/signin" method="post">
        <input type="text" name="username" placeholder="Username" required>
        <input type="password" name="password" placeholder="Password" required>
        <button type="submit">Sign In</button>
      </form>
    </main>
    {% endblock %}
    :root {
      /* Primary colors */
      --primary-color: #7c3aed;
      --primary-hover: #6d28d9;
    
      /* Background colors */
      --bg-primary: #0f172a;
      --bg-secondary: #1e293b;
      --bg-card: #1e293b;
    
      /* Text colors */
      --text-primary: #f8fafc;
      --text-secondary: #94a3b8;
    
      /* Status colors */
      --success: #22c55e;
      --warning: #f59e0b;
      --error: #ef4444;
    
      /* Border radius */
      --radius-sm: 0.375rem;
      --radius-md: 0.5rem;
      --radius-lg: 0.75rem;
    }
    /* Light theme */
    .theme-light {
      --bg-primary: #ffffff;
      --bg-secondary: #f1f5f9;
      --text-primary: #0f172a;
      --text-secondary: #64748b;
    }
    TEMPLATES_PATH=/path/to/custom/templates
    # Restart server to reload templates
    docker-compose restart authority
    GET https://auth.example.com/authorize?
      response_type=code
      &client_id=abc123
      &redirect_uri=https://app.example.com/callback
      &scope=openid%20profile%20email
      &state=xyz789
    https://app.example.com/callback?code=AUTH_CODE_HERE&state=xyz789
    POST /token HTTP/1.1
    Host: auth.example.com
    Authorization: Basic YWJjMTIzOnNlY3JldA==
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=AUTH_CODE_HERE
    &redirect_uri=https://app.example.com/callback
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "scope": "openid profile email"
    }
    const express = require('express');
    const crypto = require('crypto');
    
    const app = express();
    
    const CLIENT_ID = 'your_client_id';
    const CLIENT_SECRET = 'your_client_secret';
    const REDIRECT_URI = 'http://localhost:3000/callback';
    const AUTHORITY_URL = 'https://auth.example.com';
    
    // Step 1: Redirect to authorization
    app.get('/login', (req, res) => {
      const state = crypto.randomBytes(16).toString('hex');
      req.session.state = state;
    
      const params = new URLSearchParams({
        response_type: 'code',
        client_id: CLIENT_ID,
        redirect_uri: REDIRECT_URI,
        scope: 'openid profile email',
        state: state
      });
    
      res.redirect(`${AUTHORITY_URL}/authorize?${params}`);
    });
    
    // Step 2: Handle callback
    app.get('/callback', async (req, res) => {
      const { code, state } = req.query;
    
      // Verify state
      if (state !== req.session.state) {
        return res.status(400).send('Invalid state');
      }
    
      // Exchange code for tokens
      const credentials = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64');
    
      const tokenResponse = await fetch(`${AUTHORITY_URL}/token`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Authorization': `Basic ${credentials}`
        },
        body: new URLSearchParams({
          grant_type: 'authorization_code',
          code: code,
          redirect_uri: REDIRECT_URI
        })
      });
    
      const tokens = await tokenResponse.json();
      req.session.tokens = tokens;
    
      res.redirect('/profile');
    });
    from flask import Flask, redirect, request, session
    import requests
    import secrets
    
    app = Flask(__name__)
    
    CLIENT_ID = 'your_client_id'
    CLIENT_SECRET = 'your_client_secret'
    REDIRECT_URI = 'http://localhost:5000/callback'
    AUTHORITY_URL = 'https://auth.example.com'
    
    @app.route('/login')
    def login():
        state = secrets.token_urlsafe(16)
        session['state'] = state
    
        params = {
            'response_type': 'code',
            'client_id': CLIENT_ID,
            'redirect_uri': REDIRECT_URI,
            'scope': 'openid profile email',
            'state': state
        }
    
        return redirect(f"{AUTHORITY_URL}/authorize?{urlencode(params)}")
    
    @app.route('/callback')
    def callback():
        code = request.args.get('code')
        state = request.args.get('state')
    
        if state != session.get('state'):
            return 'Invalid state', 400
    
        token_response = requests.post(
            f"{AUTHORITY_URL}/token",
            data={
                'grant_type': 'authorization_code',
                'code': code,
                'redirect_uri': REDIRECT_URI
            },
            auth=(CLIENT_ID, CLIENT_SECRET)
        )
    
        session['tokens'] = token_response.json()
        return redirect('/profile')
    {
      "error": "error_code",
      "error_description": "Human-readable description",
      "error_uri": "https://docs.example.com/errors/error_code"
    }
    https://app.example.com/callback?error=access_denied&error_description=User%20denied%20access
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    
    {
      "error": "invalid_grant",
      "error_description": "The authorization code has expired"
    }
    HTTP/1.1 400 Bad Request
    Content-Type: application/json
    
    {
      "error": "authorization_pending",
      "error_description": "The user has not yet completed authorization"
    }
    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
    Content-Type: application/json
    
    {
      "error": "invalid_token",
      "error_description": "The access token expired"
    }
    {
      "error": "validation_error",
      "error_description": "Request validation failed",
      "details": [
        {
          "field": "email",
          "message": "Email format is invalid"
        },
        {
          "field": "password",
          "message": "Password must be at least 12 characters"
        }
      ]
    }
    HTTP/1.1 429 Too Many Requests
    Retry-After: 60
    Content-Type: application/json
    
    {
      "error": "rate_limit_exceeded",
      "error_description": "Rate limit exceeded. Retry after 60 seconds.",
      "retry_after": 60
    }
    function generateCodeVerifier() {
      const array = new Uint8Array(32);
      crypto.getRandomValues(array);
      return base64URLEncode(array);
    }
    
    async function generateCodeChallenge(verifier) {
      const encoder = new TextEncoder();
      const data = encoder.encode(verifier);
      const hash = await crypto.subtle.digest('SHA-256', data);
      return base64URLEncode(new Uint8Array(hash));
    }
    
    function base64URLEncode(buffer) {
      return btoa(String.fromCharCode(...buffer))
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
    }
    import hashlib
    import base64
    import secrets
    
    def generate_code_verifier():
        return secrets.token_urlsafe(32)
    
    def generate_code_challenge(verifier):
        digest = hashlib.sha256(verifier.encode()).digest()
        return base64.urlsafe_b64encode(digest).rstrip(b'=').decode()
    GET https://auth.example.com/authorize?
      response_type=code
      &client_id=abc123
      &redirect_uri=https://app.example.com/callback
      &scope=openid%20profile%20email
      &state=xyz789
      &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      &code_challenge_method=S256
    POST /token HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=authorization_code
    &code=AUTH_CODE_HERE
    &redirect_uri=https://app.example.com/callback
    &client_id=abc123
    &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
    import { useEffect, useState } from 'react';
    
    const CLIENT_ID = 'your_client_id';
    const REDIRECT_URI = 'http://localhost:3000/callback';
    const AUTHORITY_URL = 'https://auth.example.com';
    
    function App() {
      const [user, setUser] = useState(null);
    
      useEffect(() => {
        // Check for callback
        if (window.location.search.includes('code=')) {
          handleCallback();
        }
      }, []);
    
      async function login() {
        const codeVerifier = generateCodeVerifier();
        const codeChallenge = await generateCodeChallenge(codeVerifier);
        const state = generateCodeVerifier();
    
        // Store for callback
        sessionStorage.setItem('code_verifier', codeVerifier);
        sessionStorage.setItem('state', state);
    
        const params = new URLSearchParams({
          response_type: 'code',
          client_id: CLIENT_ID,
          redirect_uri: REDIRECT_URI,
          scope: 'openid profile email',
          state: state,
          code_challenge: codeChallenge,
          code_challenge_method: 'S256'
        });
    
        window.location.href = `${AUTHORITY_URL}/authorize?${params}`;
      }
    
      async function handleCallback() {
        const params = new URLSearchParams(window.location.search);
        const code = params.get('code');
        const state = params.get('state');
    
        // Verify state
        if (state !== sessionStorage.getItem('state')) {
          throw new Error('Invalid state');
        }
    
        const codeVerifier = sessionStorage.getItem('code_verifier');
    
        // Exchange code for tokens
        const response = await fetch(`${AUTHORITY_URL}/token`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
          body: new URLSearchParams({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: REDIRECT_URI,
            client_id: CLIENT_ID,
            code_verifier: codeVerifier
          })
        });
    
        const tokens = await response.json();
    
        // Get user info
        const userResponse = await fetch(`${AUTHORITY_URL}/userinfo`, {
          headers: { 'Authorization': `Bearer ${tokens.access_token}` }
        });
    
        setUser(await userResponse.json());
    
        // Clean up
        sessionStorage.removeItem('code_verifier');
        sessionStorage.removeItem('state');
        window.history.replaceState({}, '', '/');
      }
    
      return (
        <div>
          {user ? (
            <p>Welcome, {user.name}!</p>
          ) : (
            <button onClick={login}>Login</button>
          )}
        </div>
      );
    }
    import * as AuthSession from 'expo-auth-session';
    import * as Crypto from 'expo-crypto';
    
    const CLIENT_ID = 'your_client_id';
    const AUTHORITY_URL = 'https://auth.example.com';
    
    async function login() {
      const codeVerifier = generateCodeVerifier();
      const codeChallenge = await generateCodeChallenge(codeVerifier);
    
      const redirectUri = AuthSession.makeRedirectUri();
    
      const result = await AuthSession.startAsync({
        authUrl: `${AUTHORITY_URL}/authorize?` + new URLSearchParams({
          response_type: 'code',
          client_id: CLIENT_ID,
          redirect_uri: redirectUri,
          scope: 'openid profile email',
          code_challenge: codeChallenge,
          code_challenge_method: 'S256'
        })
      });
    
      if (result.type === 'success') {
        const tokens = await exchangeCode(result.params.code, codeVerifier, redirectUri);
        return tokens;
      }
    }
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "token_type": "Bearer",
      "expires_in": 3600
    }
    POST /token HTTP/1.1
    Host: auth.example.com
    Authorization: Basic YWJjMTIzOnNlY3JldA==
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=refresh_token&refresh_token=dGhpcyBpcyBhIHJlZnJlc2g
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...",
      "token_type": "Bearer",
      "expires_in": 3600
    }
    class TokenManager {
      constructor(tokens) {
        this.tokens = tokens;
        this.expiresAt = Date.now() + (tokens.expires_in * 1000);
        this.scheduleRefresh();
      }
    
      scheduleRefresh() {
        // Refresh 5 minutes before expiry
        const refreshIn = this.expiresAt - Date.now() - (5 * 60 * 1000);
    
        if (refreshIn > 0) {
          setTimeout(() => this.refresh(), refreshIn);
        }
      }
    
      async refresh() {
        const response = await fetch('/token', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Authorization': `Basic ${btoa(`${CLIENT_ID}:${CLIENT_SECRET}`)}`
          },
          body: new URLSearchParams({
            grant_type: 'refresh_token',
            refresh_token: this.tokens.refresh_token
          })
        });
    
        this.tokens = await response.json();
        this.expiresAt = Date.now() + (this.tokens.expires_in * 1000);
        this.scheduleRefresh();
    
        return this.tokens;
      }
    
      getAccessToken() {
        return this.tokens.access_token;
      }
    }
    async function apiCall(url, options = {}) {
      let response = await fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${tokenManager.getAccessToken()}`
        }
      });
    
      if (response.status === 401) {
        // Token expired, refresh and retry
        await tokenManager.refresh();
    
        response = await fetch(url, {
          ...options,
          headers: {
            ...options.headers,
            'Authorization': `Bearer ${tokenManager.getAccessToken()}`
          }
        });
      }
    
      return response;
    }
    {
      "error": "invalid_grant",
      "error_description": "The refresh token is invalid or expired"
    }
    async function refresh() {
      const response = await fetch('/token', {
        method: 'POST',
        // ...
      });
    
      if (!response.ok) {
        const error = await response.json();
    
        if (error.error === 'invalid_grant') {
          // Refresh token invalid - require re-login
          logout();
          redirectToLogin();
          return;
        }
    
        throw new Error(error.error_description);
      }
    
      return response.json();
    }
    grant_type=refresh_token
    &refresh_token=dGhpcyBpcyBhIHJlZnJlc2g
    &scope=read  # Originally had "read write"
    scope=openid profile email offline_access
    POST /token/revoke HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    token=dGhpcyBpcyBhIHJlZnJlc2g&token_type_hint=refresh_token
    User → Your App → Authority → Your App Backend → Token
    User → Your App → Authority → Your App → Token (with PKCE)
    Your Service → Authority → Token
    Device → Authority → User on Browser → Device polls → Token
    Refresh Token - Token renewal

    temporarily_unavailable

    Server is temporarily unavailable

    Client not authorized for grant type

    unsupported_grant_type

    400

    Grant type not supported

    invalid_scope

    400

    Invalid scope requested

    Device code expired

    404

    Not Found - Resource doesn't exist

    429

    Too Many Requests - Rate limited

    500

    Server Error - Internal error

    503

    Service Unavailable - Temporarily down

    MFA verification required

    invalid_token

    Expired token

    Refresh or re-authenticate

    Evaluate trade-offs between approaches
    OpenID Connect Concepts
    Choosing Grant Types
    Architecture
    Security Model
    Token Lifecycle
    Why Authority?
    Tutorials
    How-To Guides
    Reference
    spinner
    spinner

    Logs in with new password

    Choose:
    • Send Reset Email - User receives email

    • Set Password - Admin sets new password

    PASSWORD_RESET_TTL

    3600

    Reset token lifetime (seconds)

    PASSWORD_RESET_EMAIL_SUBJECT

    Reset your password

    Email subject

    password.reset_requested

    Reset email requested

    password.reset_sent

    Reset email sent

    password.reset_completed

    Password was reset

    password.reset_failed

    Reset attempt failed

    Create Admin
    Manage Sessions
    Password Policies

    nickname

    String

    Casual name

    preferred_username

    String

    Username

    profile

    String

    Profile page URL

    picture

    String

    Profile picture URL

    website

    String

    Website URL

    gender

    String

    Gender

    birthdate

    String

    Birthday (YYYY-MM-DD)

    zoneinfo

    String

    Timezone

    locale

    String

    Locale

    updated_at

    Number

    Last updated timestamp

    sub

    String

    Subject identifier

    name

    String

    Full name

    given_name

    String

    First name

    family_name

    String

    Last name

    middle_name

    String

    Middle name

    email

    String

    Email address

    email_verified

    Boolean

    Email verified

    address

    Object

    Address object

    phone_number

    String

    Phone number

    phone_number_verified

    Boolean

    Phone verified

    Format

    JWT (signed)

    JSON

    When received

    Token response

    Separate request

    Purpose

    Authentication proof

    Additional claims

    Freshness

    Issued at auth time

    Current values

    ID Tokens
    JWKS
    Discovery
    Signing algorithms are acceptable

    issuer

    Provider identifier (base URL)

    authorization_endpoint

    URL for authorization requests

    token_endpoint

    URL for token requests

    userinfo_endpoint

    URL for user information

    jwks_uri

    URL for JSON Web Key Set

    registration_endpoint

    Dynamic client registration

    revocation_endpoint

    Token revocation

    introspection_endpoint

    Token introspection

    device_authorization_endpoint

    Device flow

    scopes_supported

    Available scopes

    response_types_supported

    Supported response types

    grant_types_supported

    Supported grant types

    token_endpoint_auth_methods_supported

    Client authentication methods

    claims_supported

    Available user claims

    JWKS
    UserInfo
    ID Tokens

    SMTP_FROM

    Sender email

    SMTP_FROM_NAME

    Authority

    Sender name

    RATE_LIMIT_WHITELIST

    Exempt IPs

    development

    Environment: development, production, test

    PORT

    4000

    HTTP server port

    HOST

    0.0.0.0

    Bind address

    BASE_URL

    http://localhost:4000

    Public URL

    CRYSTAL_WORKERS

    4

    Worker processes

    DATABASE_URL

    Required

    PostgreSQL connection string

    SECRET_KEY

    Required

    JWT signing key (256-bit)

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    CODE_TTL

    600

    Authorization code lifetime (seconds)

    DEVICE_CODE_TTL

    300

    Device code lifetime (seconds)

    SESSION_KEY

    session_id

    Session cookie name

    SESSION_DURATION_DAYS

    7

    Session lifetime (days)

    IDLE_TIMEOUT_MINUTES

    30

    Idle timeout

    SINGLE_SESSION

    false

    Allow only one session

    SSL_CERT

    Path to certificate

    SSL_KEY

    Path to private key

    SSL_CA

    Path to CA certificate

    SSL_MODE

    SSL mode

    TEMPLATES_PATH

    ./public/templates

    Template directory

    CRYSTAL_LOG_LEVEL

    debug

    Log level

    CRYSTAL_LOG_SOURCES

    *

    Log sources

    SMTP_HOST

    SMTP server host

    SMTP_PORT

    587

    SMTP server port

    SMTP_USER

    SMTP username

    SMTP_PASSWORD

    SMTP password

    REDIS_URL

    Redis connection URL

    RATE_LIMIT_ENABLED

    true

    Enable rate limiting

    RATE_LIMIT_BY

    ip

    Rate limit key

    RATE_LIMIT_WINDOW

    60

    Window (seconds)

    RATE_LIMIT_MAX

    60

    Max requests

    APP_NAME

    Authority

    Application name

    APP_TAGLINE

    Application tagline

    COMPANY_NAME

    Company name

    THEME

    dark

    Theme (dark/light)

    ENABLE_PASSWORD_GRANT

    false

    Enable password grant

    PASSWORD_GRANT_ALLOWED_CLIENTS

    Allowed client IDs

    DEFAULT_SCOPES

    openid

    Default scopes

    Security Settings
    Token Settings
    Security Settings
    Token Settings
    Environment Variables
    PKCE is better - Authorization code + PKCE is now preferred
    Authorization Code + PKCE
    OAuth 2.0 Reference
    MFA bypass - May skip multi-factor authentication

    grant_type

    Yes

    Must be password

    username

    Yes

    User's username or email

    password

    Yes

    User's password

    scope

    Optional

    Authorization Code + PKCE
    Authorization Code
    Client Credentials

    Requested scopes

    Choose Consumer or Business depending on your use case
  • Click Next

  • Fill in app details:

    • App name: Your application name

    • App contact email: Your email

    • Click Create App

  • Add Facebook Login product:

    • Find Facebook Login in products

    • Click Set Up

    • Choose Web

    • Enter your site URL

  • Configure OAuth settings:

    • Go to Facebook Login > Settings

    • Add to Valid OAuth Redirect URIs:

    • Save changes

  • Get credentials:

    • Go to Settings > Basic

    • Copy App ID and App Secret

  • Enter your credentials:
    • Client ID: Your Facebook App ID

    • Client Secret: Your Facebook App Secret

  • Save settings

  • Complete annual Data Use Checkup

    id

    Facebook user ID

    email

    User's email (if permitted)

    name

    Full name

    picture

    Profile picture URL

    Facebook Developersarrow-up-right
    Configure Google
    Manage Linked Accounts

    Client proves identity with code_verifier

    Authority shows login page. User enters credentials.
  • User authorizes

    Authority shows consent screen. User approves scopes.

  • Authorization Server redirects back

  • Client exchanges code for tokens

  • Client receives tokens

  • Client uses access token

  • Client verifies state matches

  • Resource Owner

    The user who owns the data

    Client

    The application requesting access

    Authorization Server

    Issues tokens after authorization

    Resource Server

    Hosts the protected resources (API)

    Authorization Code

    Exchanged for tokens

    Minutes

    Access Token

    Authorizes API requests

    Hours

    Refresh Token

    Gets new access tokens

    Days/Weeks

    OpenID Connect Concepts
    Choosing Grant Types
    Token Lifecycle
    Subject: Reset your password
    
    Click the link below to reset your password:
    https://auth.example.com/reset-password?token=abc123...
    
    This link expires in 1 hour.
    
    If you didn't request this, ignore this email.
    {% extends "emails/base.html" %}
    
    {% block content %}
    <h1>Reset Your Password</h1>
    <p>Hi {{ user.name }},</p>
    <p>Click the button below to reset your password:</p>
    <a href="{{ reset_url }}" class="button">Reset Password</a>
    <p>This link expires in {{ expiry_minutes }} minutes.</p>
    {% endblock %}
    POST /api/users/{id}/send-password-reset
    Authorization: Bearer {admin_token}
    PATCH /api/users/{id}
    Content-Type: application/json
    Authorization: Bearer {admin_token}
    
    {
      "password": "NewSecurePassword123"
    }
    PASSWORD_RESET_RATE_LIMIT=3
    PASSWORD_RESET_RATE_WINDOW=3600
    HIDE_EMAIL_EXISTENCE=true
    If an account with that email exists, a reset link has been sent.
    {% extends "layout.html" %}
    {% set title = "Reset Password" %}
    
    {% block body %}
    <main class="reset-form">
      <h1>Choose a New Password</h1>
      <form action="/reset-password" method="post">
        <input type="hidden" name="token" value="{{ token }}">
        <input type="password" name="password" placeholder="New Password" required>
        <input type="password" name="password_confirmation" placeholder="Confirm Password" required>
        <button type="submit">Reset Password</button>
      </form>
    </main>
    {% endblock %}
    <h1>Password Reset Complete</h1>
    <p>Your password has been updated.</p>
    <a href="/signin">Sign In</a>
    // Request reset
    const response = await fetch('/api/password-reset', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: 'user@example.com' })
    });
    
    // Complete reset
    const response = await fetch('/api/password-reset/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        token: 'reset_token_from_email',
        password: 'NewPassword123'
      })
    });
    GET /userinfo HTTP/1.1
    Host: auth.example.com
    Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
    POST /userinfo HTTP/1.1
    Host: auth.example.com
    Content-Type: application/x-www-form-urlencoded
    
    access_token=eyJhbGciOiJSUzI1NiIs...
    {
      "sub": "user-uuid",
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "preferred_username": "johnd",
      "email": "john@example.com",
      "email_verified": true,
      "picture": "https://example.com/johnd/photo.jpg",
      "locale": "en-US",
      "updated_at": 1699996399
    }
    {
      "formatted": "123 Main St\nCity, State 12345",
      "street_address": "123 Main St",
      "locality": "City",
      "region": "State",
      "postal_code": "12345",
      "country": "US"
    }
    async function getUserInfo(accessToken) {
      const response = await fetch('https://auth.example.com/userinfo', {
        headers: {
          'Authorization': `Bearer ${accessToken}`
        }
      });
    
      if (!response.ok) {
        throw new Error('Failed to fetch user info');
      }
    
      return response.json();
    }
    
    // Usage
    const user = await getUserInfo(tokens.access_token);
    console.log(`Hello, ${user.name}!`);
    import requests
    
    def get_userinfo(access_token):
        response = requests.get(
            'https://auth.example.com/userinfo',
            headers={'Authorization': f'Bearer {access_token}'}
        )
    
        if response.status_code != 200:
            raise Exception('Failed to fetch user info')
    
        return response.json()
    
    user = get_userinfo(tokens['access_token'])
    print(f"Hello, {user['name']}!")
    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Bearer error="invalid_token", error_description="The access token is invalid"
    
    {
      "error": "invalid_token",
      "error_description": "The access token is invalid"
    }
    HTTP/1.1 401 Unauthorized
    WWW-Authenticate: Bearer error="invalid_token", error_description="The access token has expired"
    
    {
      "error": "invalid_token",
      "error_description": "The access token has expired"
    }
    HTTP/1.1 403 Forbidden
    WWW-Authenticate: Bearer error="insufficient_scope", scope="openid profile"
    
    {
      "error": "insufficient_scope",
      "error_description": "The access token lacks the required scope"
    }
    class UserInfoCache {
      constructor(ttl = 60000) { // 1 minute
        this.cache = new Map();
        this.ttl = ttl;
      }
    
      async get(accessToken) {
        const cached = this.cache.get(accessToken);
        if (cached && Date.now() < cached.expiresAt) {
          return cached.data;
        }
    
        const data = await fetchUserInfo(accessToken);
        this.cache.set(accessToken, {
          data,
          expiresAt: Date.now() + this.ttl
        });
    
        return data;
      }
    }
    {
      "issuer": "https://auth.example.com",
      "authorization_endpoint": "https://auth.example.com/authorize",
      "token_endpoint": "https://auth.example.com/token",
      "userinfo_endpoint": "https://auth.example.com/userinfo",
      "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
      "registration_endpoint": "https://auth.example.com/register",
      "revocation_endpoint": "https://auth.example.com/token/revoke",
      "introspection_endpoint": "https://auth.example.com/token/introspect",
      "device_authorization_endpoint": "https://auth.example.com/device",
    
      "scopes_supported": [
        "openid",
        "profile",
        "email",
        "address",
        "phone",
        "offline_access"
      ],
    
      "response_types_supported": [
        "code",
        "token",
        "id_token",
        "code token",
        "code id_token",
        "token id_token",
        "code token id_token"
      ],
    
      "response_modes_supported": [
        "query",
        "fragment"
      ],
    
      "grant_types_supported": [
        "authorization_code",
        "refresh_token",
        "client_credentials",
        "urn:ietf:params:oauth:grant-type:device_code"
      ],
    
      "subject_types_supported": [
        "public"
      ],
    
      "id_token_signing_alg_values_supported": [
        "RS256"
      ],
    
      "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "none"
      ],
    
      "claims_supported": [
        "sub",
        "iss",
        "aud",
        "exp",
        "iat",
        "auth_time",
        "nonce",
        "name",
        "given_name",
        "family_name",
        "email",
        "email_verified",
        "picture",
        "locale"
      ],
    
      "code_challenge_methods_supported": [
        "S256",
        "plain"
      ]
    }
    async function discoverProvider(issuer) {
      const response = await fetch(
        `${issuer}/.well-known/openid-configuration`
      );
      return response.json();
    }
    
    // Use discovered configuration
    const config = await discoverProvider('https://auth.example.com');
    
    // Now use the endpoints
    const authUrl = new URL(config.authorization_endpoint);
    authUrl.searchParams.set('client_id', CLIENT_ID);
    authUrl.searchParams.set('response_type', 'code');
    // ...
    import requests
    
    def discover_provider(issuer):
        response = requests.get(
            f"{issuer}/.well-known/openid-configuration"
        )
        return response.json()
    
    config = discover_provider('https://auth.example.com')
    print(f"Auth endpoint: {config['authorization_endpoint']}")
    class ProviderConfig {
      constructor(issuer) {
        this.issuer = issuer;
        this.config = null;
        this.expiresAt = null;
      }
    
      async getConfig() {
        // Cache for 1 hour
        if (this.config && Date.now() < this.expiresAt) {
          return this.config;
        }
    
        const response = await fetch(
          `${this.issuer}/.well-known/openid-configuration`
        );
        this.config = await response.json();
        this.expiresAt = Date.now() + (60 * 60 * 1000);
    
        return this.config;
      }
    }
    function validateConfig(config, expectedIssuer) {
      if (config.issuer !== expectedIssuer) {
        throw new Error('Issuer mismatch');
      }
    
      if (!config.authorization_endpoint) {
        throw new Error('Missing authorization endpoint');
      }
    
      if (!config.scopes_supported.includes('openid')) {
        throw new Error('OpenID scope not supported');
      }
    
      return true;
    }
    CRYSTAL_ENV=development
    PORT=4000
    BASE_URL=http://localhost:4000
    DATABASE_URL=postgres://localhost:5432/authority_db
    SECRET_KEY=dev-secret-key
    CRYSTAL_LOG_LEVEL=debug
    CRYSTAL_ENV=production
    PORT=4000
    BASE_URL=https://auth.example.com
    DATABASE_URL=postgres://user:pass@db:5432/authority_db?sslmode=require
    SECRET_KEY=your-production-secret-key
    CRYSTAL_WORKERS=8
    CRYSTAL_LOG_LEVEL=info
    
    # Security
    LOCKOUT_THRESHOLD=5
    LOCKOUT_DURATION=30
    PASSWORD_MIN_LENGTH=12
    REQUIRE_ADMIN_MFA=true
    
    # Tokens
    ACCESS_TOKEN_TTL=3600
    REFRESH_TOKEN_TTL=2592000
    
    # Redis
    REDIS_URL=redis://redis:6379
    
    # Email
    SMTP_HOST=smtp.example.com
    SMTP_PORT=587
    SMTP_USER=noreply@example.com
    SMTP_PASSWORD=your-smtp-password
    SMTP_FROM=noreply@example.com
    User → Client → Authority → Client (with token in URL fragment)
    GET /authorize?
      response_type=token
      &client_id=abc123
      &redirect_uri=https://app.example.com/callback
      &scope=openid%20profile
      &state=xyz789
    https://app.example.com/callback#
      access_token=eyJhbGciOiJSUzI1NiIs...
      &token_type=Bearer
      &expires_in=3600
      &state=xyz789
    // Redirect with response_type=token
    window.location = '/authorize?response_type=token&client_id=...';
    
    // Get token from URL fragment
    const token = new URLSearchParams(window.location.hash.slice(1)).get('access_token');
    // Generate PKCE values
    const verifier = generateCodeVerifier();
    const challenge = await generateCodeChallenge(verifier);
    
    // Redirect with response_type=code
    window.location = `/authorize?response_type=code&client_id=...&code_challenge=${challenge}&code_challenge_method=S256`;
    
    // Exchange code for token
    const tokenResponse = await fetch('/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        code_verifier: verifier,
        client_id: CLIENT_ID
      })
    });
    POST /token HTTP/1.1
    Host: auth.example.com
    Authorization: Basic YWJjMTIzOnNlY3JldA==
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=password
    &username=user@example.com
    &password=secret123
    &scope=openid%20profile
    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "scope": "openid profile"
    }
    ENABLE_PASSWORD_GRANT=true
    PASSWORD_GRANT_ALLOWED_CLIENTS=trusted-app-1,trusted-app-2
    // Instead of collecting password in app
    // Redirect to Authority's login page
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: 'openid profile',
      code_challenge: challenge,
      code_challenge_method: 'S256'
    });
    
    // Open in-app browser
    openAuthUrl(`${AUTHORITY_URL}/authorize?${params}`);
    // Redirect to Authority for login
    window.location = `${AUTHORITY_URL}/authorize?response_type=code&...`;
    https://your-authority-domain/auth/facebook/callback
    FACEBOOK_OAUTH_ENABLED=true
    FACEBOOK_CLIENT_ID=your-facebook-app-id
    FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
    <a href="https://your-authority-domain/auth/facebook" class="btn-facebook">
      <svg><!-- Facebook icon --></svg>
      Continue with Facebook
    </a>
    const forwardUrl = btoa('https://your-app.com/dashboard');
    const facebookAuthUrl = `https://your-authority-domain/auth/facebook?forward_url=${forwardUrl}`;
    https://your-authority-domain/auth/facebook/callback
    https://app.example.com/callback?
      code=AUTH_CODE_HERE
      &state=xyz789
    POST /token
    grant_type=authorization_code
    &code=AUTH_CODE_HERE
    &redirect_uri=https://app.example.com/callback
    &client_id=abc123
    &client_secret=secret123
    {
      "access_token": "eyJhbG...",
      "refresh_token": "dGhpc...",
      "expires_in": 3600
    }
    GET /api/user
    Authorization: Bearer eyJhbG...
    ┌──────────────────────────────────────────────────┐
    │                                                  │
    │   Resource Owner         Authorization Server    │
    │   (User)                 (Authority)             │
    │      │                        │                  │
    │      │ authorizes             │ issues tokens    │
    │      ▼                        ▼                  │
    │   Client ◄─────────────────────────────────►     │
    │   (App)                  Resource Server         │
    │                          (API)                   │
    │                                                  │
    └──────────────────────────────────────────────────┘
    Full User Data
    ├── profile (name, picture)
    ├── email (email address)
    ├── read (read-only access)
    ├── write (modify data)
    └── admin (administrative access)
    scope=profile email read
    "App X wants to access your profile and email"
    User → Client → Authority → User (login) → Client (code) → Authority → Client (tokens)
    Service → Authority (client_id + secret) → Service (token)
    Device → Authority (get code)
    Device → User: "Go to URL, enter code"
    User → Authority (enter code, approve)
    Device → Authority (poll for token)
    GET /authorize?
      response_type=code
      &client_id=abc123
      &redirect_uri=https://app.example.com/callback
      &scope=profile email
      &state=xyz789
                                Attacker
                                   │
    ┌─────────┐                    │                    ┌─────────┐
    │ Client  │───code_challenge───┼───────────────────►│Authority│
    │         │◄──authorization_code◄──────────────────│         │
    │         │───code_verifier────┼────────────────────►│         │
    │         │◄──access_token─────┼────────────────────│         │
    └─────────┘                    │                    └─────────┘
                                   │
                        Attacker has code,
                        but can't prove
                        they know verifier
    ID Tokens - Token specification

    /.well-known/openid-configuration

    Discovery document

    /.well-known/jwks.json

    Public keys for verification

    /userinfo

    User claims endpoint

    /authorize

    Authorization (with OIDC scopes)

    /token

    Token (returns ID token)

    openid

    sub (required for OIDC)

    profile

    name, family_name, given_name, picture, etc.

    email

    email, email_verified

    address

    address

    phone

    phone_number, phone_number_verified

    Purpose

    Authorization

    Authentication

    Token

    Access token

    ID token

    Question answered

    "What can they do?"

    "Who are they?"

    Scope

    Custom scopes

    ID Tokens
    UserInfo
    Discovery
    OpenID Connect Core 1.0arrow-up-right
    OpenID Connect Discovery 1.0arrow-up-right
    OpenID Connect Dynamic Client Registration 1.0arrow-up-right
    Discovery
    JWKS
    UserInfo

    openid, profile, etc.

    client_id

    Client identifier

    nonce

    Request nonce

    at_hash

    Access token hash

    3600

    Access token lifetime (seconds)

    REFRESH_TOKEN_TTL

    2592000

    Refresh token lifetime (30 days)

    CODE_TTL

    600

    Authorization code lifetime (10 min)

    DEVICE_CODE_TTL

    300

    Device code lifetime (5 min)

    ID_TOKEN_TTL

    3600

    ID token lifetime (1 hour)

    REFRESH_TOKEN_ROTATION

    true

    Rotate on refresh

    REFRESH_TOKEN_REUSE_INTERVAL

    0

    Grace period for reuse (seconds)

    REFRESH_TOKEN_ABSOLUTE_TTL

    31536000

    Absolute lifetime (1 year)

    ACCESS_TOKEN_FORMAT

    jwt

    jwt or opaque

    JWT_ALGORITHM

    RS256

    Signing algorithm

    iss

    Issuer (Authority URL)

    sub

    Subject (user ID)

    aud

    Audience (client ID)

    exp

    Expiration time

    iat

    Issued at time

    scope

    Granted scopes

    iss

    Issuer

    sub

    Subject

    aud

    Audience

    exp

    Expiration

    iat

    Issued at

    auth_time

    Authentication time

    INTROSPECTION_CACHE_TTL

    60

    Cache introspection results

    REVOKE_REFRESH_ON_PASSWORD_CHANGE

    true

    Revoke on password change

    REVOKE_ALL_ON_LOGOUT

    false

    Revoke all tokens on logout

    Security Settings
    All Options
    Token Lifecycle

    Authentication

    Question

    "What can they do?"

    "Who are they?"

    Result

    Access token

    ID token + Access token

    Use case

    API access

    User login

    OpenID Connect (OIDC) is a thin layer on top of OAuth 2.0 that adds identity.

    hashtag
    The Identity Layer

    OIDC adds:

    • ID Token - Proof of authentication

    • UserInfo Endpoint - User profile data

    • Standard Scopes - openid, profile, email

    • Discovery - Automatic configuration

    hashtag
    The ID Token

    The ID token is a JWT that proves authentication:

    hashtag
    Key Claims

    Claim
    Description

    iss

    Who issued the token

    sub

    Unique user identifier

    aud

    Who the token is for

    exp

    When it expires

    iat

    When it was issued

    auth_time

    When user authenticated

    hashtag
    Standard Scopes

    Request identity information with scopes:

    Scope
    Claims

    openid

    sub (required)

    profile

    name, picture, etc.

    email

    email, email_verified

    address

    address

    phone

    phone_number

    hashtag
    Authentication Flow

    hashtag
    ID Token vs Access Token

    Aspect
    ID Token
    Access Token

    For

    Client

    Resource Server

    Purpose

    Prove identity

    Authorize API calls

    Validate

    At login

    On each request

    Contains

    User identity

    Authorization grants

    hashtag
    When to Use Which

    hashtag
    UserInfo Endpoint

    Get more user data with the access token:

    Response:

    hashtag
    ID Token vs UserInfo

    Aspect
    ID Token
    UserInfo

    When

    Token response

    Separate request

    Format

    JWT (signed)

    JSON

    Freshness

    Point-in-time

    Current values

    Use ID token for authentication proof. Use UserInfo for current profile data.

    hashtag
    Discovery

    OIDC providers publish configuration:

    Response:

    Clients use discovery to automatically configure themselves.

    hashtag
    Nonce

    Prevents token replay attacks:

    1. Client generates random nonce

    2. Client sends nonce in authorization request

    3. Authority includes nonce in ID token

    4. Client verifies nonce matches

    hashtag
    Authentication Patterns

    hashtag
    Simple Login

    hashtag
    Single Sign-On (SSO)

    hashtag
    Silent Authentication

    hashtag
    Security Considerations

    hashtag
    Validate ID Tokens

    Always validate:

    1. Signature - Using JWKS

    2. Issuer - Matches expected

    3. Audience - Contains your client ID

    4. Expiration - Not expired

    5. Nonce - If used, matches sent value

    hashtag
    Don't Trust Claims Blindly

    • ID tokens are signed, not encrypted

    • Claims can be read by anyone

    • Don't put sensitive data in ID tokens

    hashtag
    Use ID Token for Authentication Only

    • ID tokens prove identity at login time

    • Don't use ID tokens for API authorization

    • Use access tokens for API calls

    hashtag
    Next Steps

    • OAuth 2.0 Concepts - Authorization fundamentals

    • Token Lifecycle - Token management

    • ID Tokens Reference - Technical details

    Purpose

    Authorization

    hashtag
    Endpoints Layer

    The endpoints layer handles HTTP requests and responses:

    • OAuth Endpoints - Authorization, token, introspection, revocation

    • OIDC Endpoints - UserInfo, discovery, JWKS

    • Admin Endpoints - Client, user, scope management

    • Auth Endpoints - Login, logout, password reset

    hashtag
    Services Layer

    Business logic is encapsulated in services:

    Service
    Responsibility

    AuthenticationService

    User authentication, MFA validation

    TokenService

    Token generation, validation, revocation

    UserService

    User CRUD, password management

    ClientService

    OAuth client management

    SessionService

    Session creation, validation, cleanup

    AuditService

    Event logging, compliance

    hashtag
    Data Layer

    Data access and persistence:

    • Models - Data structures (User, Client, Token, etc.)

    • Repositories - Database queries

    • Migrations - Schema management

    hashtag
    Request Flow

    hashtag
    Authorization Code Flow

    hashtag
    Token Validation Flow

    hashtag
    Data Model

    hashtag
    Core Entities

    hashtag
    User

    hashtag
    Client

    hashtag
    Security Architecture

    hashtag
    Defense in Depth

    hashtag
    Token Security

    1. Signing - JWTs signed with RS256

    2. Rotation - Refresh tokens rotated on use

    3. Revocation - Tokens can be invalidated

    4. Expiration - Short-lived access tokens

    hashtag
    Scalability

    hashtag
    Horizontal Scaling

    Authority supports multiple instances:

    hashtag
    Redis for State

    Shared state stored in Redis:

    • User sessions

    • Rate limit counters

    • Token cache (optional)

    hashtag
    Technology Stack

    Layer
    Technology

    Language

    Crystal

    Web Framework

    Azu

    Database

    PostgreSQL

    Cache

    Redis

    Templates

    Crinja (Jinja2)

    JWT

    crystal-jwt

    hashtag
    Next Steps

    • Security Model - Security architecture

    • Token Lifecycle - Token management

    • OAuth 2.0 Concepts - Protocol fundamentals

    Token Structure

    ID tokens are JWTs with three parts:

    hashtag
    Decoded Token

    hashtag
    Header

    hashtag
    Payload

    hashtag
    Standard Claims

    hashtag
    Required Claims

    Claim
    Description

    iss

    Issuer (Authority URL)

    sub

    Subject (unique user ID)

    aud

    Audience (client ID)

    exp

    Expiration time

    iat

    Issued at time

    hashtag
    Optional Claims

    Claim
    Description

    auth_time

    Time of authentication

    nonce

    Value from authorization request

    at_hash

    Access token hash

    c_hash

    Code hash (for hybrid flow)

    acr

    Authentication context class

    amr

    Authentication methods used

    hashtag
    Profile Claims

    Claim
    Description

    name

    Full name

    given_name

    First name

    family_name

    Last name

    email

    Email address

    email_verified

    Email verified

    picture

    Profile picture URL

    hashtag
    Validation

    hashtag
    Required Steps

    1. Verify signature using JWKS

    2. Check iss matches expected issuer

    3. Check aud contains your client ID

    4. Check exp is in the future

    5. Check iat is reasonable

    6. Check nonce matches sent value (if used)

    hashtag
    JavaScript Example

    hashtag
    Python Example

    hashtag
    Using Nonce

    Nonce prevents replay attacks:

    hashtag
    Request

    hashtag
    Validation

    hashtag
    at_hash Validation

    The at_hash claim allows ID token to be bound to access token:

    hashtag
    Common Issues

    hashtag
    "Invalid signature"

    • Check JWKS URL is correct

    • Verify key ID (kid) matches

    • Ensure algorithm is RS256

    hashtag
    "Token expired"

    • Check server/client time sync

    • Token may have short lifetime

    • Refresh authentication

    hashtag
    "Invalid audience"

    • Verify client ID in validation

    • Check token was issued for your client

    hashtag
    Next Steps

    • JWKS - Token verification keys

    • UserInfo - Additional claims

    • Discovery - Provider configuration

    Complete OAuth 2.0 implementation
  • OpenID Connect identity layer

  • Enterprise security features

  • Modern admin dashboard

  • Self-hosted deployment

  • hashtag
    Why Self-Host Authentication?

    hashtag
    Data Sovereignty

    Your user data stays on your infrastructure:

    • Full control over data location

    • Compliance with data residency requirements

    • No third-party data access

    • Custom retention policies

    hashtag
    Cost Predictability

    No per-user or per-authentication pricing:

    • Fixed infrastructure costs

    • Scale without cost surprises

    • No vendor lock-in

    • Budget predictability

    hashtag
    Customization

    Complete control over the experience:

    • Custom login pages

    • Branded emails

    • Custom flows

    • Extended functionality

    hashtag
    Security Control

    Your security, your way:

    • Configure security policies

    • Custom audit requirements

    • Integration with existing security tools

    • Incident response control

    hashtag
    Key Features

    hashtag
    Complete OAuth 2.0

    All standard grant types:

    Grant Type
    Use Case

    Authorization Code

    Web applications

    Authorization Code + PKCE

    Mobile / SPA

    Client Credentials

    Server-to-server

    Device Code

    IoT / CLI

    Refresh Token

    Token renewal

    hashtag
    OpenID Connect

    Full identity support:

    • ID tokens for authentication

    • UserInfo endpoint

    • Discovery document

    • JWKS for verification

    hashtag
    Enterprise Security

    Production-ready security:

    • Multi-factor authentication (TOTP)

    • Account lockout

    • Password policies

    • Session management

    • Comprehensive audit logging

    • Rate limiting

    hashtag
    Admin Dashboard

    Modern management interface:

    • OAuth client management

    • User administration

    • Scope configuration

    • Audit log viewer

    • System settings

    hashtag
    Use Cases

    hashtag
    SaaS Applications

    Build authentication for your SaaS product:

    • Single sign-on across services

    • User management

    • Third-party integrations

    hashtag
    Internal Tools

    Secure internal applications:

    • Employee authentication

    • Service-to-service auth

    • API gateway integration

    • Audit compliance

    hashtag
    Developer Platforms

    Power developer ecosystems:

    • OAuth for third-party apps

    • API access control

    • Developer portal integration

    • Rate limiting per client

    hashtag
    IoT / Device Authentication

    Authenticate devices and CLIs:

    • Device code flow

    • Machine credentials

    • Token management

    hashtag
    Comparison

    hashtag
    vs. Auth0 / Okta

    Aspect
    Authority
    Auth0/Okta

    Hosting

    Self-hosted

    Cloud

    Pricing

    Infrastructure only

    Per user

    Data location

    Your servers

    Their servers

    Customization

    Full

    Limited

    hashtag
    vs. Keycloak

    Aspect
    Authority
    Keycloak

    Language

    Crystal

    Java

    Memory footprint

    Low

    High

    Complexity

    Simple

    Complex

    Features

    Core OAuth/OIDC

    Enterprise IAM

    hashtag
    vs. Build Your Own

    Aspect
    Authority
    Custom

    Time to production

    Hours

    Months

    Security review

    Done

    Required

    Maintenance

    Updates

    All on you

    Standards compliance

    Complete

    Variable

    hashtag
    Getting Started

    hashtag
    Quick Start

    hashtag
    Production Deployment

    1. Configure environment

    2. Set up database

    3. Enable HTTPS

    4. Create admin user

    5. Register clients

    See Installation Guide.

    hashtag
    Standards Compliance

    Authority implements:

    • RFC 6749arrow-up-right - OAuth 2.0

    • RFC 6750arrow-up-right - Bearer Tokens

    • RFC 7519arrow-up-right - JWT

    • - PKCE

    • - Introspection

    • - Revocation

    • - Device Authorization

    hashtag
    Technology

    Built with modern, efficient technology:

    Component
    Technology

    Language

    Crystal

    Framework

    Azu

    Database

    PostgreSQL

    Caching

    Redis

    Templates

    Crinja

    Crystal provides:

    • Performance - Near C speed

    • Safety - Type-safe, null-safe

    • Simplicity - Ruby-like syntax

    • Efficiency - Low memory footprint

    hashtag
    Next Steps

    • Quick Start Tutorial - Get running in 5 minutes

    • Architecture - System design

    • Security Model - Security features

    Failed attempts before lockout

    LOCKOUT_DURATION

    30

    Lockout duration (minutes)

    ENABLE_AUTO_UNLOCK

    true

    Auto-unlock after duration

    PROGRESSIVE_LOCKOUT

    false

    Increase duration with each lockout

    LOCKOUT_BY_IP

    false

    Lock by IP instead of account

    IP_LOCKOUT_THRESHOLD

    10

    IP-based lockout threshold

    LOCKOUT_WHITELIST

    IPs exempt from lockout

    hashtag
    Password Policies

    Variable
    Default
    Description

    PASSWORD_MIN_LENGTH

    12

    Minimum password length

    PASSWORD_HISTORY_COUNT

    5

    Prevent password reuse

    PASSWORD_EXPIRY_DAYS

    0

    Password expiration (0=never)

    PASSWORD_EXPIRY_WARNING_DAYS

    14

    Warn before expiry

    hashtag
    Admin Password Policies

    Variable
    Default
    Description

    ADMIN_PASSWORD_MIN_LENGTH

    16

    Admin minimum length

    ADMIN_PASSWORD_EXPIRY_DAYS

    30

    Admin expiration

    hashtag
    Multi-Factor Authentication

    Variable
    Default
    Description

    REQUIRE_MFA

    false

    Require MFA for all users

    REQUIRE_ADMIN_MFA

    true

    Require MFA for admins

    MFA_GRACE_PERIOD_DAYS

    7

    Time to set up MFA

    hashtag
    Session Security

    Variable
    Default
    Description

    SESSION_DURATION_DAYS

    7

    Maximum session lifetime

    IDLE_TIMEOUT_MINUTES

    30

    Idle timeout

    SINGLE_SESSION

    false

    Only one active session

    NOTIFY_NEW_SESSION

    false

    Email on new login

    hashtag
    Access Control

    Variable
    Default
    Description

    ADMIN_ALLOWED_IPS

    IP whitelist for admin

    HIDE_EMAIL_EXISTENCE

    true

    Don't reveal if email exists

    hashtag
    Password Reset

    Variable
    Default
    Description

    PASSWORD_RESET_TTL

    3600

    Reset token lifetime (seconds)

    PASSWORD_RESET_RATE_LIMIT

    3

    Max resets per window

    PASSWORD_RESET_RATE_WINDOW

    3600

    Rate limit window (seconds)

    hashtag
    JWT Security

    Variable
    Default
    Description

    SECRET_KEY

    Required

    JWT signing key

    JWT_ALGORITHM

    RS256

    Signing algorithm

    hashtag
    Audit Logging

    Variable
    Default
    Description

    AUDIT_LOG_RETENTION_DAYS

    90

    Log retention period

    AUDIT_LOG_SYSLOG

    false

    Send to syslog

    SYSLOG_HOST

    Syslog server

    SYSLOG_PORT

    514

    Syslog port

    hashtag
    Example: High Security

    hashtag
    Example: User-Friendly

    hashtag
    Next Steps

    • Token Settings - Token configuration

    • All Options - Complete reference

    • Enable MFA - MFA setup

    LOCKOUT_THRESHOLD

    5

    hashtag
    Authorization Code

    hashtag
    Purpose

    Short-lived code exchanged for tokens. Separates user authentication from token issuance.

    hashtag
    Lifecycle

    hashtag
    Security Properties

    • One-time use - Cannot be reused

    • Short-lived - Minimizes interception window

    • Client-bound - Tied to specific client

    • PKCE-protected - Proof of possession (public clients)

    hashtag
    Access Token

    hashtag
    Purpose

    Authorizes API requests. Presented to resource servers.

    hashtag
    Lifecycle

    hashtag
    Format

    Authority issues JWTs:

    hashtag
    Validation

    Resource servers validate by:

    1. Verifying JWT signature (JWKS)

    2. Checking expiration

    3. Verifying issuer and audience

    4. Checking required scopes

    hashtag
    Refresh Token

    hashtag
    Purpose

    Obtains new access tokens without user interaction.

    hashtag
    Lifecycle

    hashtag
    Rotation

    With rotation enabled:

    hashtag
    Grace Period

    Optional reuse window for concurrent requests:

    hashtag
    ID Token

    hashtag
    Purpose

    Proves user authentication. Contains identity claims.

    hashtag
    Lifecycle

    hashtag
    Validation

    Must validate:

    1. Signature (JWKS)

    2. Issuer matches expected

    3. Audience contains client ID

    4. Expiration not passed

    5. Nonce matches (if sent)

    hashtag
    Token Revocation

    hashtag
    Triggers

    Tokens are revoked when:

    • User logs out

    • User changes password

    • Admin revokes manually

    • Security incident detected

    • Session ends

    hashtag
    Cascade

    hashtag
    Revocation Check

    For real-time validation:

    hashtag
    Token Storage

    hashtag
    Server-Side

    hashtag
    Client-Side

    Token
    Storage
    Notes

    Access

    Memory

    Clear on logout

    Refresh

    Secure cookie

    httpOnly, secure

    ID

    Memory

    User info display

    hashtag
    Best Practices

    hashtag
    Token Lifetimes

    Environment
    Access Token
    Refresh Token

    High Security

    15 minutes

    1 day

    Standard

    1 hour

    30 days

    User-Friendly

    1 day

    90 days

    hashtag
    Refresh Strategy

    Proactive:

    Reactive:

    hashtag
    Handling Expiration

    hashtag
    Next Steps

    • Security Model - Security architecture

    • Refresh Tokens Reference - Technical details

    • Token Settings - Configuration

    Space-separated scopes

    state

    Recommended

    CSRF protection

    code_challenge

    Yes

    Base64URL(SHA256(verifier))

    code_challenge_method

    Yes

    S256

    Client identifier

    code_verifier

    Yes

    Original code verifier

    High

    Device Code

    IoT, CLI, Smart TVs

    Public

    Medium

    Refresh Token

    Token renewal

    Both

    High

    Implicit

    Legacy SPAs

    Public

    Low

    Password

    Trusted first-party

    Confidential

    Medium

    /device

    POST

    Start device flow

    RFC 7662arrow-up-right
    RFC 7009arrow-up-right
    RFC 8628arrow-up-right
    Device Flow
    Authorization Code
    Authorization Code + PKCE
    Client Credentials
    {
      "iss": "https://auth.example.com",
      "sub": "user-uuid",
      "aud": "client-id",
      "exp": 1699999999,
      "iat": 1699996399,
      "auth_time": 1699996300,
      "nonce": "abc123",
      "name": "John Doe",
      "email": "john@example.com",
      "email_verified": true
    }
    {
      "sub": "user-uuid",
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "email": "john@example.com",
      "email_verified": true,
      "picture": "https://..."
    }
    GET /.well-known/openid-configuration
    // 1. Request authorization with openid scope
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: 'openid profile email',
      state: state,
      nonce: nonce  // For ID token validation
    });
    
    window.location = `${AUTHORITY_URL}/authorize?${params}`;
    
    // 2. Exchange code for tokens
    const tokens = await fetch('/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code: code,
        redirect_uri: REDIRECT_URI
      })
    }).then(r => r.json());
    
    // 3. Validate and use ID token
    const idToken = parseIdToken(tokens.id_token);
    console.log('User:', idToken.name);
    
    // 4. Optionally fetch more claims
    const userinfo = await fetch('/userinfo', {
      headers: { 'Authorization': `Bearer ${tokens.access_token}` }
    }).then(r => r.json());
    {
      "client_id": "my-client",
      "access_token_ttl": 7200,
      "refresh_token_ttl": 604800,
      "refresh_token_rotation": true
    }
    ACCESS_TOKEN_TTL=900          # 15 minutes
    REFRESH_TOKEN_TTL=86400       # 1 day
    REFRESH_TOKEN_ROTATION=true
    CODE_TTL=300                  # 5 minutes
    ACCESS_TOKEN_TTL=86400        # 1 day
    REFRESH_TOKEN_TTL=7776000     # 90 days
    REFRESH_TOKEN_ROTATION=false
    ACCESS_TOKEN_TTL=3600         # 1 hour
    REFRESH_TOKEN_TTL=0           # Disabled
    ┌────────────────────────────────────────┐
    │           OpenID Connect               │
    │         (Authentication)               │
    ├────────────────────────────────────────┤
    │              OAuth 2.0                 │
    │          (Authorization)               │
    └────────────────────────────────────────┘
    {
      "iss": "https://auth.example.com",
      "sub": "user-123",
      "aud": "client-abc",
      "exp": 1699999999,
      "iat": 1699996399,
      "auth_time": 1699996300,
      "nonce": "abc123",
      "name": "John Doe",
      "email": "john@example.com"
    }
    scope=openid profile email
    ID Token: "This is John, authenticated 5 minutes ago"
               → Use for login decisions
    
    Access Token: "John has read access to documents"
                  → Use for API authorization
    GET /userinfo
    Authorization: Bearer access_token
    {
      "sub": "user-123",
      "name": "John Doe",
      "given_name": "John",
      "family_name": "Doe",
      "email": "john@example.com",
      "picture": "https://..."
    }
    GET /.well-known/openid-configuration
    {
      "issuer": "https://auth.example.com",
      "authorization_endpoint": "https://auth.example.com/authorize",
      "token_endpoint": "https://auth.example.com/token",
      "userinfo_endpoint": "https://auth.example.com/userinfo",
      "jwks_uri": "https://auth.example.com/.well-known/jwks.json"
    }
    User clicks "Login" → Redirect to Authority → User logs in →
    Redirect back with tokens → User is logged in
    User logged into Authority → Visits App A → Already authenticated →
    Visits App B → Already authenticated (no re-login)
    Client → Authority (prompt=none) →
    If logged in: Return tokens
    If not: Return error
    users
    ├── id (UUID)
    ├── email (unique)
    ├── password_hash
    ├── name
    ├── mfa_secret
    ├── mfa_enabled
    ├── locked
    ├── locked_at
    ├── failed_attempts
    ├── role
    ├── created_at
    └── updated_at
    clients
    ├── id (UUID)
    ├── client_id (unique)
    ├── client_secret_hash
    ├── name
    ├── redirect_uris (array)
    ├── grant_types (array)
    ├── scopes (array)
    ├── client_type
    ├── created_at
    └── updated_at
    ┌─────────────────────────────────────┐
    │           Rate Limiting              │
    ├─────────────────────────────────────┤
    │          Input Validation            │
    ├─────────────────────────────────────┤
    │         Authentication               │
    ├─────────────────────────────────────┤
    │         Authorization                │
    ├─────────────────────────────────────┤
    │         Business Logic               │
    ├─────────────────────────────────────┤
    │         Data Validation              │
    ├─────────────────────────────────────┤
    │         Audit Logging                │
    └─────────────────────────────────────┘
                        ┌─────────────┐
                        │   Load      │
                        │  Balancer   │
                        └──────┬──────┘
                ┌──────────────┼──────────────┐
          ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐
          │ Authority │  │ Authority │  │ Authority │
          │ Instance  │  │ Instance  │  │ Instance  │
          └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
                └──────────────┼──────────────┘
                        ┌──────┴──────┐
          ┌─────────────┴─────────────┴─────────────┐
          │              PostgreSQL                   │
          │              (Primary)                    │
          └─────────────┬─────────────┬─────────────┘
                        │             │
                  ┌─────┴─────┐ ┌─────┴─────┐
                  │  Replica  │ │  Replica  │
                  └───────────┘ └───────────┘
    eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGhvcml0eS1rZXktMSJ9.
    eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiJ1c2VyLXV1aWQi...
    .signature
    {
      "alg": "RS256",
      "typ": "JWT",
      "kid": "authority-key-1"
    }
    {
      "iss": "https://auth.example.com",
      "sub": "user-uuid",
      "aud": "client-id",
      "exp": 1699999999,
      "iat": 1699996399,
      "auth_time": 1699996300,
      "nonce": "n-0S6_WzA2Mj",
      "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
      "name": "John Doe",
      "email": "john@example.com",
      "email_verified": true
    }
    const jwt = require('jsonwebtoken');
    const jwksClient = require('jwks-rsa');
    
    const client = jwksClient({
      jwksUri: 'https://auth.example.com/.well-known/jwks.json'
    });
    
    async function validateIdToken(idToken, expectedNonce) {
      return new Promise((resolve, reject) => {
        const getKey = (header, callback) => {
          client.getSigningKey(header.kid, (err, key) => {
            callback(err, key?.getPublicKey());
          });
        };
    
        jwt.verify(idToken, getKey, {
          algorithms: ['RS256'],
          issuer: 'https://auth.example.com',
          audience: 'your_client_id'
        }, (err, decoded) => {
          if (err) {
            reject(err);
            return;
          }
    
          // Check nonce
          if (expectedNonce && decoded.nonce !== expectedNonce) {
            reject(new Error('Invalid nonce'));
            return;
          }
    
          // Check auth_time if max_age was requested
          // Check other claims as needed
    
          resolve(decoded);
        });
      });
    }
    import jwt
    from jwt import PyJWKClient
    
    jwks_client = PyJWKClient("https://auth.example.com/.well-known/jwks.json")
    
    def validate_id_token(id_token, expected_nonce=None):
        signing_key = jwks_client.get_signing_key_from_jwt(id_token)
    
        claims = jwt.decode(
            id_token,
            signing_key.key,
            algorithms=["RS256"],
            issuer="https://auth.example.com",
            audience="your_client_id"
        )
    
        if expected_nonce and claims.get('nonce') != expected_nonce:
            raise ValueError('Invalid nonce')
    
        return claims
    const nonce = generateRandomString();
    sessionStorage.setItem('oidc_nonce', nonce);
    
    const params = new URLSearchParams({
      response_type: 'code',
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: 'openid profile email',
      nonce: nonce
    });
    const savedNonce = sessionStorage.getItem('oidc_nonce');
    const claims = await validateIdToken(tokens.id_token, savedNonce);
    sessionStorage.removeItem('oidc_nonce');
    function validateAtHash(idToken, accessToken) {
      const claims = jwt.decode(idToken);
    
      if (claims.at_hash) {
        const hash = crypto.createHash('sha256').update(accessToken).digest();
        const expectedHash = hash.slice(0, hash.length / 2);
        const calculatedAtHash = base64url.encode(expectedHash);
    
        if (claims.at_hash !== calculatedAtHash) {
          throw new Error('at_hash mismatch');
        }
      }
    }
    Your App → Authority → User Login → Your App
                        → API Access
    # Clone and run
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    docker-compose up -d
    
    # Visit http://localhost:4000
    # Strict account lockout
    LOCKOUT_THRESHOLD=3
    LOCKOUT_DURATION=60
    PROGRESSIVE_LOCKOUT=true
    
    # Strong passwords
    PASSWORD_MIN_LENGTH=16
    REQUIRE_UPPERCASE=true
    REQUIRE_LOWERCASE=true
    REQUIRE_NUMBERS=true
    REQUIRE_SPECIAL=true
    PASSWORD_EXPIRY_DAYS=90
    CHECK_COMMON_PASSWORDS=true
    
    # MFA required
    REQUIRE_MFA=true
    REQUIRE_ADMIN_MFA=true
    
    # Short sessions
    SESSION_DURATION_DAYS=1
    IDLE_TIMEOUT_MINUTES=15
    SINGLE_SESSION=true
    
    # Notifications
    NOTIFY_NEW_SESSION=true
    
    # IP restrictions
    ADMIN_ALLOWED_IPS=10.0.0.0/8
    # Lenient lockout
    LOCKOUT_THRESHOLD=10
    LOCKOUT_DURATION=15
    ENABLE_AUTO_UNLOCK=true
    
    # Reasonable passwords
    PASSWORD_MIN_LENGTH=10
    REQUIRE_UPPERCASE=false
    REQUIRE_LOWERCASE=false
    REQUIRE_NUMBERS=false
    PASSWORD_EXPIRY_DAYS=0
    
    # MFA optional except admins
    REQUIRE_MFA=false
    REQUIRE_ADMIN_MFA=true
    
    # Longer sessions
    SESSION_DURATION_DAYS=30
    IDLE_TIMEOUT_MINUTES=60
    Creation:  User approves consent
    Lifetime:  10 minutes (configurable)
    Exchange:  POST /token (one-time use)
    Deletion:  After exchange or expiration
    Creation:  Token endpoint (code exchange or refresh)
    Lifetime:  1 hour (configurable)
    Usage:     Bearer token in API requests
    Expiration: Rejected after TTL
    {
      "iss": "https://auth.example.com",
      "sub": "user-123",
      "aud": "client-abc",
      "exp": 1699999999,
      "scope": "read write"
    }
    Creation:  Token endpoint (with offline_access scope)
    Lifetime:  30 days (configurable)
    Usage:     POST /token with grant_type=refresh_token
    Rotation:  New refresh token on each use
    Time T:   Client has Refresh Token A
    Time T+1: Client uses Refresh Token A
              ↓
              New Access Token issued
              New Refresh Token B issued
              Refresh Token A invalidated
    
    Time T+2: Attacker tries Refresh Token A
              ↓
              Rejected (already used)
              Alert: Possible token theft
    Refresh Token A used at T
    Refresh Token A still valid for 2 seconds
    After 2 seconds: Only Refresh Token B valid
    Creation:  Token endpoint (with openid scope)
    Lifetime:  1 hour (typically)
    Usage:     Validate at login time
    Storage:   Client-side (for user info display)
    Session Revoked
    ├── Access Tokens invalidated
    └── Refresh Tokens invalidated
    POST /token/introspect
    ↓
    {
      "active": false  // Token revoked
    }
    ┌─────────────────────────────────────────┐
    │            PostgreSQL                    │
    │  ┌─────────┐ ┌─────────┐ ┌─────────┐   │
    │  │ Access  │ │ Refresh │ │  Auth   │   │
    │  │ Tokens  │ │ Tokens  │ │  Codes  │   │
    │  └─────────┘ └─────────┘ └─────────┘   │
    └─────────────────────────────────────────┘
    
    ┌─────────────────────────────────────────┐
    │              Redis                       │
    │  ┌─────────────────────────────────┐    │
    │  │   Token Cache (optional)         │    │
    │  │   Session Storage                │    │
    │  └─────────────────────────────────┘    │
    └─────────────────────────────────────────┘
    // Refresh 5 minutes before expiry
    scheduleRefresh(expiresIn - 300);
    // Refresh on 401 response
    if (response.status === 401) {
      await refresh();
      retry();
    }
    async function apiCall(endpoint) {
      // Check token validity
      if (isExpired(accessToken)) {
        accessToken = await refresh();
      }
    
      return fetch(endpoint, {
        headers: { Authorization: `Bearer ${accessToken}` }
      });
    }
    spinner

    azp

    Authorized party

    PASSWORD_EXPIRY_GRACE_DAYS

    7

    Grace period

    REQUIRE_UPPERCASE

    true

    Require uppercase

    REQUIRE_LOWERCASE

    true

    Require lowercase

    REQUIRE_NUMBERS

    true

    Require numbers

    REQUIRE_SPECIAL

    false

    Require special chars

    CHECK_COMMON_PASSWORDS

    false

    Check against list

    COMMON_PASSWORD_LIST

    Path to password list

    REVOKE_TOKENS_ON_SESSION_END

    true

    Revoke tokens on logout

    Vendor lock-in

    None

    High

    Learning curve

    Gentle

    Steep

    RFC 7636arrow-up-right
    RFC 7662arrow-up-right
    RFC 7009arrow-up-right
    RFC 8628arrow-up-right
    OpenID Connect Core 1.0arrow-up-right
    spinner

    nonce

    Prevents replay attacks

    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner