Only this pageAll pages
Powered 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...

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.

Get Authority running in 5 minutes:

See for a complete walkthrough.

Category
Features

Getting Started

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

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

Prerequisites: Docker installed Duration: 5 minutes

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

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

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

Quick Start

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

  • installed

  • installed

This starts:

  • Authority server on port 4000

First OAuth Integration

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

  • Completed

  • Authority running on http://localhost:4000

  • An OAuth client created with:

Protect Your API

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

  • Completed

  • A backend API you want to protect

  • Validate access tokens server-side

Prerequisites: Completed First OAuth Integration Duration: 25 minutes

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

  • OpenID Connect user authentication

  • Scope-based access control

If you get stuck:

  1. Check the How-To Guides for specific tasks

  2. Review the Reference for technical details

  3. Read the Explanation for conceptual understanding

Available Tutorials

Quick Start

First OAuth Integration

Protect Your API

Add User Authentication

What You'll Learn

Getting Help

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

This documentation is organized using the Diataxis framework:

Step-by-step guides for learning Authority:

  • Quick Start - Get running in 5 minutes

  • First OAuth Integration - Build your first OAuth app

  • Protect Your API - Secure your endpoints

  • Add User Authentication - Implement login flows

Task-oriented guides for specific goals:

  • Installation - Docker, source, Kubernetes

  • Configuration - Environment setup

  • Security - MFA, lockout, passwords

  • OAuth Clients - Client management

Technical specifications and API documentation:

  • OAuth 2.0 Flows - Grant type specifications

  • OpenID Connect - OIDC endpoints

  • API Endpoints - Complete API reference

  • Configuration - All settings

Understanding concepts and architecture:

  • Architecture - System design

  • OAuth 2.0 Concepts - Protocol fundamentals

  • Security Model - Security architecture

  • Choosing Grant Types - Decision guide

Authority implements these specifications:

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

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

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

Build the authorization URL and redirect the user:

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

Make a POST request to the token endpoint:

The response contains:

Include the token in API requests:

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

  • Protect Your API - Validate tokens on your server

  • Authorization Code Reference - Full specification

  • PKCE Reference - PKCE details

Prerequisites

Quick Start
// 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>

What You'll Build

The Authorization Code Flow

Step 1: Generate PKCE Values

Step 2: Redirect to Authorization Endpoint

PostgreSQL database on port 5432

Open your browser to http://localhost:4000.

You should see the Authority landing page:

Landing Page

Click Sign In and use the default admin credentials:

  • Username: admin@example.com

  • Password: password123

Sign In

After signing in, you can:

  • Manage OAuth Clients - Register applications

  • Manage Users - Create and edit accounts

  • Configure Scopes - Define access permissions

  • View Audit Logs - Track all actions

  • Adjust Settings - Configure security policies

Admin Dashboard
  1. Navigate to OAuth Clients

  2. Click New Client

  3. Fill in:

    • Name: My Test App

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

  4. Click Create

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

  • First OAuth Integration - Build a complete OAuth flow

  • Docker Installation Guide - Production deployment options

  • Environment Variables - Configuration options

Stop the conflicting service or change the port:

Ensure PostgreSQL is running:

Check Authority logs:

git clone https://github.com/azutoolkit/authority.git
cd authority
docker-compose up -d

Prerequisites

Step 1: Clone the Repository

Step 2: Start Authority

Docker
Docker Compose
PORT=4001 docker-compose up -d
docker-compose logs db
docker-compose logs authority

Step 3: Access the Dashboard

Step 4: Sign In

Step 5: Explore the Admin Dashboard

Step 6: Create Your First OAuth Client

Next Steps

Troubleshooting

Port 4000 is in use

Database connection failed

Can't access the dashboard

Implement scope-based access control

  • Handle token expiration gracefully

  • 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

    Approach
    Use When

    Local validation

    Low latency required, stateless validation

    Token introspection

    Need real-time revocation checks, opaque tokens

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

    Node.js Example:

    Python Example:

    Express.js Middleware:

    For real-time validation, use the introspection endpoint.

    Token introspection requires client authentication:

    The response:

    If the token is invalid or revoked:

    Configure scopes in Authority's admin dashboard:

    Scope
    Description

    read

    Read-only access

    write

    Create and update resources

    admin

    • Add User Authentication - OpenID Connect integration

    • Token Introspection Reference - Full specification

    • Refresh Tokens - Token renewal

    Prerequisites

    What You'll Learn

    First OAuth Integration
    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 });
    });

    Installation

    Deploy Authority using Docker for production environments.

    Prerequisites

    • Docker 20.10+

    • Docker Compose 2.0+

    • 1GB RAM minimum

    • PostgreSQL (included in docker-compose)

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

    Create a .env file:

    Create docker-compose.yml:

    Run migrations manually if needed:

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

    Check logs:

    Verify PostgreSQL is running:

    Change the port:

    • - All configuration options

    • - Enable HTTPS

    • - Session storage

    Add Social Login

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

    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

    Prerequisites

    • Authority running locally or deployed

    • A Google account for testing

    • Basic web development knowledge

    1. Go to

    2. Create a new project:

      • Click the project dropdown at the top

      • Click New Project

    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:

    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

    4. Sign in with your Google account

    Add a sign-in button to your app:

    After successful authentication, redirect users to your app:

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

    Now that Google is working, add more providers:

    1. GitHub:

    2. Facebook:

    3. LinkedIn:

    4. Apple:

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

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

    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.

    Check that your forward_url is correctly encoded.

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

    • - Let users link multiple providers

    • - Add extra security

    • - Full OAuth client setup

    Add User Authentication

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

    Prerequisites

    • Completed First OAuth Integration

    • Understanding of OAuth 2.0 authorization code flow

    What You'll Learn

    • Use OpenID Connect for user authentication

    • Parse and validate ID tokens

    • Display user profile information

    • Handle sessions and logout

    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.

    Add openid and profile scopes to your authorization request:

    Scope
    Claims Included

    The token response now includes an ID token:

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

    Always validate ID tokens before trusting them:

    Use the UserInfo endpoint for more claims:

    Authority supports OpenID Connect RP-Initiated Logout:

    • - Full specification

    • - Token structure

    • - Available claims

    From Source

    Build and run Authority from source code.

    Prerequisites

    • Crystal 1.9+

    • PostgreSQL 13+

    • Git

    Step 1: Clone Repository

    git clone https://github.com/azutoolkit/authority.git
    cd authority

    Step 2: Install Dependencies

    shards install

    Step 3: Set Up Database

    Create the database:

    createdb authority_db

    Or using psql:

    psql -c "CREATE DATABASE authority_db;"

    Step 4: Configure Environment

    Copy the example environment file:

    Edit .env.local:

    Create an admin user:

    Development mode:

    Or with hot reload:

    Run specific tests:

    Use watchexec or similar:

    Ensure Crystal is in your PATH:

    Start PostgreSQL:

    Update shards cache:

    Check what's using the port:

    • - Containerized deployment

    • - Database configuration

    • - All settings

    Kubernetes

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

    Prerequisites

    • Kubernetes cluster (1.24+)

    • kubectl configured

    • Helm 3 (optional)

    Quick Start with Manifests

    1. Create Namespace

    # namespace.yaml
    apiVersion: v1
    kind: Namespace
    metadata:
      name: authority
    kubectl apply -f namespace.yaml

    2. Create Secrets

    # 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)

    3. Deploy PostgreSQL

    # 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

    4. Deploy Authority

    # 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

    5. Configure Ingress

    # 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

    6. Apply All Manifests

    kubectl apply -f namespace.yaml
    kubectl apply -f postgres.yaml
    kubectl apply -f authority.yaml
    kubectl apply -f ingress.yaml

    Helm Chart

    Add Repository

    helm repo add authority https://azutoolkit.github.io/authority-helm
    helm repo update

    Install

    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

    Custom Values

    Create values.yaml:

    Install with values:

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

    View logs:

    • - Configuration options

    • - TLS configuration

    • - Multi-factor authentication

    Configuration

    Configure Authority using environment variables.

    Server Settings

    Variable
    Default
    Description

    CRYSTAL_ENV

    development

    Environment: development, production, test

    Variable
    Default
    Description

    Example connection strings:

    Variable
    Default
    Description

    Generate a secure key:

    Variable
    Default
    Description
    Variable
    Default
    Description
    Variable
    Default
    Description
    Variable
    Default
    Description
    Variable
    Default
    Description

    Authority automatically loads .env.local in development:

    Or with env file:

    • - Database configuration

    • - Session storage

    • - HTTPS setup

    Database Setup

    Configure PostgreSQL for Authority.

    Requirements

    • PostgreSQL 13 or higher

    • 100MB minimum disk space

    • Recommended: SSD storage

    Creating the Database

    Using psql

    # 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;"

    Using createdb

    createdb authority_db

    Connection String

    Configure the DATABASE_URL environment variable:

    # 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

    SSL Mode

    For production, enable SSL:

    SSL modes:

    • disable - No SSL

    • require - SSL required, no verification

    • verify-ca - Verify server certificate

    Migrations create the required tables:

    Table
    Description

    Create initial data:

    This creates:

    • Default admin user

    • Common OAuth scopes

    • System settings

    With compression:

    From compressed:

    For production, configure connection pooling:

    Update connection string:

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

    Check PostgreSQL is running:

    Verify credentials and pg_hba.conf:

    Create the database:

    Grant permissions:

    • - All configuration options

    • - Session storage

    • - Containerized database

    Redis Caching

    Configure Redis for session storage and caching.

    Why Use Redis?

    • Session storage - Share sessions across multiple Authority instances

    • Token caching - Faster token validation

    • Rate limiting - Distributed rate limit counters

    Prerequisites

    • Redis 6.0+

    • Network access between Authority and Redis

    Docker:

    macOS:

    Ubuntu:

    Set the Redis URL:

    With password:

    With database number:

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

    Session expiry is handled automatically by Redis TTL.

    Access tokens are cached for faster validation:

    Rate limit counters use Redis:

    Check Redis is running:

    Verify password:

    Check memory usage:

    Increase maxmemory or use eviction policy.

    • - All configuration options

    • - Containerized setup

    • - Scalable deployment

    SSL Certificates

    Enable HTTPS for Authority.

    Overview

    There are two approaches to enable HTTPS:

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

    2. Direct SSL - Authority handles SSL directly

    Option 1: Reverse Proxy (Recommended)

    Nginx with Let's Encrypt

    Install Certbot:

    sudo apt install certbot python3-certbot-nginx

    Get certificate:

    sudo certbot --nginx -d auth.example.com

    Nginx configuration:

    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;
    }

    Update Authority configuration:

    Docker labels:

    Authority can handle SSL directly using environment variables.

    Certbot sets up automatic renewal. Test with:

    Create renewal hook to reload Authority:

    Add security headers in your reverse proxy:

    Test your SSL setup:

    Include the full chain:

    Ensure Authority can read certificates:

    Ensure BASE_URL uses https://:

    • - All configuration options

    • - Production setup

    • - Additional security

    Security

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

    Overview

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

    • Google Authenticator

    • Authy

    • 1Password

    • Microsoft Authenticator

    1. Log in to Authority

    2. Click your profile name

    3. Select Security Settings

    1. Click Enable Two-Factor Authentication

    2. Authority displays a QR code

    1. Open your authenticator app

    2. Tap Add Account or +

    3. Scan the QR code

    1. Enter the 6-digit code from your app

    2. Click Verify

    3. Save your backup codes in a secure location

    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.

    Enforce MFA for all administrator accounts:

    Force MFA enrollment for all users:

    Allow users time to set up MFA:

    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

    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.

    Response:

    During OAuth authorization, check if MFA is required:

    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

    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

    • Verify device time is synchronized

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

    • Wait for the next code cycle (30 seconds)

    • Use the manual entry option

    • Enter the secret key directly into your authenticator

    • Use a backup code

    • Contact administrator to disable MFA

    • - Brute-force protection

    • - Password requirements

    • - Track security events

    Account Lockout

    Protect against brute-force attacks with account lockout.

    How It Works

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

    Configuration

    Environment Variables

    Variable
    Default
    Description

    LOCKOUT_THRESHOLD

    5

    For enhanced security, increase lockout duration with each lockout:

    1. Navigate to Admin Dashboard → Users

    2. Filter by Status: Locked

    1. Select the locked user

    2. Click Unlock Account

    3. Optionally, send password reset email

    Lockout events are logged:

    Event
    Description

    Set up alerts for lockout events:

    Response:

    Optionally lock by IP address instead of account:

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

    Exclude certain IPs from lockout:

    Users see a clear message when locked:

    Offer password reset as an alternative:

    • Check for password manager issues

    • Verify caps lock behavior

    • Consider increasing threshold

    • Verify environment variables are loaded

    • Check ENABLE_LOCKOUT is not set to false

    • Review audit logs for failed attempts

    • Verify ENABLE_AUTO_UNLOCK=true

    • Check server time synchronization

    • Review logs for errors

    • - Multi-factor authentication

    • - Password requirements

    • - Track security events

    Password Policies

    Configure password requirements for Authority users.

    Configuration

    Environment Variables

    Variable
    Default
    Description

    PASSWORD_MIN_LENGTH

    12

    Minimum password length

    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)

    Users see clear feedback:

    When PASSWORD_EXPIRY_DAYS is set, users must change passwords periodically.

    Users are warned before expiry:

    Allow logins during grace period:

    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

    Prevent password reuse:

    If a user tries to reuse a password:

    Block commonly used passwords:

    Download a password list:

    Response:

    Response:

    The UI shows password strength in real-time:

    • Weak - Red, doesn't meet requirements

    • Fair - Yellow, meets minimum

    • Strong - Green, exceeds requirements

    Offer a password generator:

    • Check which rules are enabled

    • Verify character encoding

    • Review password history

    • Consider reducing complexity requirements

    • Enable password manager hints

    • Implement "forgot password" flow

    • - Multi-factor authentication

    • - Brute-force protection

    • - Track security events

    Audit Logging

    Track all security-relevant actions in Authority.

    Authority logs all significant events to help with:

    • Security monitoring

    • Compliance requirements

    • Incident investigation

    Configure GitHub

    Enable users to sign in with their GitHub accounts.

    • Authority instance running

    • Admin access to Authority dashboard

    • GitHub account

    Configure Facebook

    Enable users to sign in with their Facebook accounts.

    • Authority instance running

    • Admin access to Authority dashboard

    • Facebook Developer account

    Configure LinkedIn

    Enable users to sign in with their LinkedIn accounts.

    • Authority instance running

    • Admin access to Authority dashboard

    • LinkedIn Developer account

    Configure Apple

    Enable users to sign in with their Apple ID.

    • Authority instance running

    • Admin access to Authority dashboard

    • Apple Developer account ($99/year membership required)

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

    Manage Linked Accounts

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

    Users can:

    • Link multiple social providers to one account

    • Unlink social providers they no longer want

    • Sign in with any linked provider

    OAuth Clients

    Create and configure OAuth clients in Authority.

    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

    Configure Scopes

    Create and manage OAuth scopes for access control.

    Scopes define what access a client can request:

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

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

    Authority includes these standard scopes:

    Social Login

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

    • - Enable Google Sign-In

    • - Enable GitHub authentication

    • - Enable Facebook Login

    Configure Google

    Enable users to sign in with their Google accounts.

    • Authority instance running

    • Admin access to Authority dashboard

    • Google Cloud Console account

    Token Validation Approaches

    When to Use Each

    Approach 1: Local JWT Validation

    Step 1: Fetch the JWKS

    Step 2: Validate the Token

    Step 3: Create Middleware

    Approach 2: Token Introspection

    Step 1: Configure Client Credentials

    Step 2: Introspect the Token

    Scope-Based Access Control

    Define Scopes

    Check Scopes in Middleware

    Handling Token Expiration

    Client-Side: Refresh Before Expiry

    Server-Side: Return Clear Errors

    Next Steps

    Administrative operations

    • RFC 6749 - OAuth 2.0 Authorization Framework

    • RFC 6750 - Bearer Token Usage

    • RFC 7519 - JSON Web Token (JWT)

    • RFC 7636 - Proof Key for Code Exchange (PKCE)

    • RFC 7662 - Token Introspection

    • RFC 7009 - Token Revocation

    • RFC 8628 - Device Authorization Grant

    • OpenID Connect Core 1.0

    Component
    Technology

    Language

    Crystal

    Web Framework

    Azu

    Database

    PostgreSQL

    Templating

    Crinja (Jinja2-compatible)

    Caching

    Redis (optional)

    Landing Page
    Admin Dashboard

    Quick Start

    Key Features

    Quick Start Tutorial

    OAuth 2.0

    Documentation Overview

    Tutorials

    How-To Guides

    Reference

    Explanation

    Standards Compliance

    Technology Stack

    Screenshots

    Quick Start

    Production Configuration

    1. Create Environment File

    2. Generate Secret Key

    3. Docker Compose File

    4. Start Services

    Reverse Proxy Setup

    Nginx Configuration

    Traefik Configuration

    Database Migrations

    Backup and Restore

    Backup Database

    Restore Database

    Monitoring

    Health Check

    View Logs

    Resource Usage

    Scaling

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

    Troubleshooting

    Container won't start

    Database connection failed

    Port already in use

    Next Steps

    Environment Variables
    SSL Certificates
    Redis Caching

    Step 5: Run Migrations

    Step 6: Seed Default Data (Optional)

    Step 7: Start the Server

    Production Build

    Compile Release Binary

    Run Production Server

    Running Tests

    Development Workflow

    Watch for Changes

    Database Reset

    File Structure

    Common Issues

    Crystal not found

    PostgreSQL connection refused

    Shards install fails

    Port already in use

    Next Steps

    Docker Installation
    Database Setup
    Environment Variables

    High Availability

    Database

    Redis for Sessions

    Pod Disruption Budget

    Monitoring

    ServiceMonitor (Prometheus)

    Logs

    Troubleshooting

    Check Pod Status

    Database Connection Issues

    View Events

    Next Steps

    Environment Variables
    SSL Certificates
    Enable MFA

    DEVICE_CODE_TTL

    300

    Device code lifetime (seconds)

    SSL_CA

    Path to CA certificate

    SSL_MODE

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

    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

    DATABASE_URL

    Required

    PostgreSQL connection string

    SECRET_KEY

    Required

    JWT signing key (256-bit minimum)

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    CODE_TTL

    600

    SESSION_KEY

    session_id

    Session cookie name

    SESSION_DURATION_DAYS

    7

    SSL_CERT

    Path to SSL certificate

    SSL_KEY

    TEMPLATES_PATH

    ./public/templates

    Path to Jinja templates

    CRYSTAL_LOG_LEVEL

    debug

    Log level: debug, info, warn, error

    CRYSTAL_LOG_SOURCES

    *

    Database

    Security

    Token Lifetimes

    Session

    SSL/TLS

    Templates

    Logging

    Example Configuration

    Development

    Production

    Loading Environment Variables

    From File

    From Shell

    Docker

    Next Steps

    Database Setup
    Redis Caching
    SSL Certificates

    Authorization code lifetime (seconds)

    Session lifetime (days)

    Path to SSL private key

    Log sources filter

    verify-full - Verify server certificate and hostname

    refresh_tokens

    Refresh tokens

    authorization_codes

    Authorization codes

    device_codes

    Device authorization codes

    scopes

    OAuth scopes

    audit_logs

    Audit trail

    sessions

    User sessions

    users

    User accounts

    clients

    OAuth clients

    access_tokens

    Running Migrations

    Tables Created

    Seeding Data

    Backup and Restore

    Backup

    Restore

    Connection Pooling

    Using PgBouncer

    Performance Tuning

    PostgreSQL Configuration

    Indexes

    Troubleshooting

    Connection refused

    Authentication failed

    Database does not exist

    Permission denied

    Next Steps

    Environment Variables
    Redis Caching
    Docker Installation

    Access tokens

    Basic Setup

    Install Redis

    Configure Authority

    Production Configuration

    Redis Configuration

    TLS/SSL

    Docker Compose

    High Availability

    Redis Sentinel

    Redis Cluster

    Session Storage

    Token Caching

    Rate Limiting

    Monitoring

    Redis CLI

    Check Connection

    Troubleshooting

    Connection refused

    Authentication failed

    Memory full

    Next Steps

    Environment Variables
    Docker Installation
    Kubernetes Deployment

    Traefik with Let's Encrypt

    Option 2: Direct SSL

    Generate Self-Signed Certificate (Development)

    Configure Authority

    Using Let's Encrypt Certificates

    Certificate Renewal

    Certbot Auto-Renewal

    Manual Renewal Hook

    Security Headers

    Verify SSL Configuration

    Troubleshooting

    Certificate chain incomplete

    Permission denied

    Mixed content warnings

    Next Steps

    Environment Variables
    Docker Installation
    Enable MFA

    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

    REQUIRE_LOWERCASE

    true

    Require lowercase letters

    REQUIRE_NUMBERS

    true

    Require numeric digits

    REQUIRE_SPECIAL

    false

    Require special characters

    Example Configurations

    Standard Security

    High Security

    User-Friendly

    Password Validation

    Validation Messages

    Password Expiry

    Expiry Warning

    Grace Period

    Handling Expired Passwords

    Password History

    Common Password Check

    API Integration

    Validate Password

    Check Password Expiry

    User Experience

    Password Strength Indicator

    Password Generator

    Best Practices

    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

    Avoid:

    • Very short minimum lengths (< 8)

    • Forced rotation without reason

    • Overly complex rules that encourage weak patterns

    Troubleshooting

    Password rejected unexpectedly

    Users forgetting complex passwords

    Next Steps

    Enable MFA
    Account Lockout
    Audit Logging
    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);
    });
    # Clone the repository
    git clone https://github.com/azutoolkit/authority.git
    cd authority
    
    # Start with Docker
    docker-compose up -d
    
    # Visit http://localhost:4000
    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
    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
    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
    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;
    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
    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
    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;
    }

    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

      • Developer contact: 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

  • After approval, you'll be redirected back to Authority

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

  • <!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

    Step 1: Create a Google OAuth App

    Step 2: Configure Authority

    Step 3: Test the Integration

    Step 4: Add to Your Application

    Step 5: Handle the Redirect

    What Happens Behind the Scenes

    Adding More Providers

    Common Issues

    "redirect_uri_mismatch"

    "Access blocked: App not verified"

    User Created But No Session

    Next Steps

    Google Cloud Console
    Configure GitHub
    Configure Facebook
    Configure LinkedIn
    Configure Apple
    Manage Linked Accounts
    Enable MFA
    First OAuth Integration

    address

    address

    phone

    phone_number, phone_number_verified

    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'
    });

    openid

    sub (required for OIDC)

    profile

    name, family_name, given_name, picture

    email

    {
      "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);
      });
    }

    OpenID Connect Overview

    Step 1: Request OpenID Scopes

    Available Scopes

    Step 2: Parse the ID Token

    Step 3: Validate the ID Token

    Step 4: Fetch Additional User Info

    Step 5: Implement Session Management

    Store Session Securely

    Display User Profile

    Step 6: Implement Logout

    Client-Side Logout

    RP-Initiated Logout (Optional)

    Complete Implementation

    Next Steps

    OpenID Connect Reference
    ID Tokens
    UserInfo Endpoint

    email, email_verified

    Failed attempts before lockout

    LOCKOUT_DURATION

    30

    Lockout duration (minutes)

    ENABLE_AUTO_UNLOCK

    true

    Auto-unlock after duration

    # 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

    user.login_failed

    Failed login attempt

    user.locked

    Account locked

    user.unlocked

    # 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]

    Example Configuration

    Progressive Lockout

    Admin Dashboard

    View Locked Accounts

    Unlock an Account

    Monitoring

    Audit Log Events

    Alerts

    API Integration

    Check Lock Status

    Unlock User

    IP-Based Lockout

    Whitelist

    User Experience

    Locked Account Message

    Reset Password Option

    Best Practices

    Recommended settings for most deployments:

    • Threshold: 5 attempts

    • Duration: 30 minutes

    • Auto-unlock: Enabled

    • Progressive lockout: Enabled

    Avoid:

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

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

    • Permanent lockout - requires manual intervention

    Troubleshooting

    Users getting locked frequently

    Lockout not working

    Auto-unlock not happening

    Next Steps

    Enable MFA
    Password Policies
    Audit Logging
    Admin Users

    Account unlocked (auto or manual)

    User activity tracking

    Event
    Description

    user.login

    Successful login

    user.login_failed

    Failed login attempt

    Each log entry includes:

    Field
    Description

    id

    Unique log entry ID

    timestamp

    When the event occurred

    event

    Example entry:

    1. Navigate to Admin Dashboard → Audit Logs

    2. Use filters to find specific events

    Audit Logs
    • 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

    Export logs in various formats:

    • CSV - For spreadsheet analysis

    • JSON - For processing with scripts

    • PDF - For reports

    Response:

    Export logs to security information systems:

    Monitor for:

    • Multiple failed logins from same IP

    • Admin account logins from new IPs

    • MFA disabled events

    • Client secret rotations

    Audit logs help demonstrate:

    • Who accessed user data

    • What changes were made

    • When access occurred

    Logs provide evidence of:

    • Access controls

    • Monitoring activities

    • Incident response

    • Enable MFA - Multi-factor authentication

    • Account Lockout - Brute-force protection

    • Password Policies - Password requirements

    Overview

    {
      "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"'

    Logged Events

    Log Entry Format

    Viewing Logs

    Admin Dashboard

    Filter Options

    Export

    API Access

    List Logs

    Filter Logs

    Get Single Entry

    Log Retention

    Configuration

    Manual Cleanup

    External Log Shipping

    Syslog

    File Output

    SIEM Integration

    Monitoring and Alerting

    Failed Login Alerts

    Suspicious Activity

    Compliance

    GDPR

    SOC 2

    Best Practices

    • Enable log shipping to external systems

    • Retain logs for at least 90 days

    • Set up alerts for critical events

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

    • Protect log access with strong controls

    • Consider log integrity (tampering detection)

    Next Steps

    Go to GitHub Developer Settings
  • Click OAuth Apps > New OAuth App

  • Fill in the application details:

    • 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)

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable GitHub OAuth

    4. Enter your credentials:

      • Client ID: Your GitHub client ID

      • Client Secret: Your GitHub client secret

    5. Save settings

    With forward URL:

    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

    5. Redirected to your application

    Field
    Description

    id

    Unique GitHub user ID

    login

    GitHub username

    email

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

    Client ID or secret is incorrect.

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

    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

    Callback URL doesn't match GitHub app configuration.

    Solution: Ensure exact match:

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

    • Configure Google - Add Google sign-in

    • Manage Linked Accounts - Link multiple providers

    Prerequisites

    Step 1: Create GitHub OAuth App

    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

    Step 2: Configure Authority

    Using Admin Dashboard

    Using Environment Variables

    Step 3: Add Login Button

    Step 4: Test the Integration

    User Data Retrieved

    GitHub Organizations

    Troubleshooting

    "Bad credentials" Error

    No Email Retrieved

    "Redirect URI mismatch"

    GitHub Enterprise

    Next Steps

    Go to Facebook Developers

  • Click My Apps > Create App

  • Select app type:

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

      https://your-authority-domain/auth/facebook/callback
    • Save changes

  • Get credentials:

    • Go to Settings > Basic

    • Copy App ID and App Secret

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable Facebook OAuth

    4. Enter your credentials:

      • Client ID: Your Facebook App ID

      • Client Secret: Your Facebook App Secret

    5. Save settings

    With forward URL:

    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.

    Field
    Description

    id

    Facebook user ID

    email

    User's email (if permitted)

    name

    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

    Redirect URI isn't whitelisted.

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

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

    Solution: Handle missing email gracefully in your application.

    Requested scopes not approved for your app.

    Solution:

    • For email and public_profile, no review needed

    • Advanced scopes require App Review

    Facebook has strict data usage policies:

    • Only request data you need

    • Explain data usage in your privacy policy

    • Delete user data upon request

    • Complete annual Data Use Checkup

    • Configure Google - Add Google sign-in

    • Manage Linked Accounts - Account linking

    Prerequisites

    Step 1: Create Facebook App

    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

    Step 2: Configure Authority

    Using Admin Dashboard

    Using Environment Variables

    Step 3: Add Login Button

    Step 4: App Review (Production)

    User Data Retrieved

    Troubleshooting

    "App Not Active"

    "URL Blocked"

    No Email Retrieved

    "Invalid Scopes"

    Privacy Considerations

    Next Steps

    Go to LinkedIn Developers

  • Click Create App

  • Fill in app details:

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

      https://your-authority-domain/auth/linkedin/callback
  • In the Products tab:

    • Request access to Sign In with LinkedIn using OpenID Connect

    • This provides openid, profile, and email scopes

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable LinkedIn OAuth

    4. Enter your credentials:

      • Client ID: Your LinkedIn Client ID

      • Client Secret: Your LinkedIn Client Secret

    5. Save settings

    With forward URL:

    Field
    Description

    sub

    LinkedIn member ID

    email

    User's email address

    email_verified

    Callback URL not registered in LinkedIn app.

    Solution: Add exact URL to Authorized redirect URLs:

    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

    Client ID is incorrect or app is deleted.

    Solution: Verify Client ID in LinkedIn Developer Console.

    Authority uses LinkedIn's OpenID Connect implementation which provides:

    • Standard OIDC claims

    • Simplified integration

    • Better long-term stability

    • Configure Google - Add more providers

    • Manage Linked Accounts - Account linking

    Prerequisites

    Step 1: Create LinkedIn App

    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

    Step 2: Configure Authority

    Using Admin Dashboard

    Using Environment Variables

    Step 3: Add Login Button

    User Data Retrieved

    Troubleshooting

    "Invalid redirect_uri"

    "Unauthorized scope"

    "Application not found"

    LinkedIn API Versions

    Next Steps

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

  • Requires Apple Developer Program membership

  • Has strict UI requirements

  • Offers email relay for privacy

    1. Go to Apple Developer Console

    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

    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://)

      • Return URLs: https://your-authority-domain/auth/apple/callback

    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

    Find your Team ID:

    • Go to Membership in Apple Developer Console

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

    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

    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

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

    5. Save settings

    Apple has specific requirements for their button:

    See Apple's Human Interface Guidelines for button requirements.

    Field
    Description

    sub

    Unique Apple user ID

    email

    User's email (real or relay)

    email_verified

    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

    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

    Callback URL not registered.

    Solution:

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

      https://your-authority-domain/auth/apple/callback
    • Ensure domain is registered (without https://)

    Authorization code expired or already used.

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

    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.

    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

    • Configure Google - Simpler provider setup

    • Manage Linked Accounts - Account linking

    Prerequisites

    Overview

    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

    Step 1: Create App ID

    Step 2: Create Services ID

    Step 3: Create Private Key

    Step 4: Get Your Team ID

    Step 5: Configure Authority

    Using Admin Dashboard

    Using Environment Variables

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

    Step 6: Add Login Button

    User Data Retrieved

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

    Email Relay

    Troubleshooting

    "Invalid client_id"

    "Invalid redirect_uri"

    "Invalid grant"

    Name Not Retrieved

    Security Considerations

    Next Steps

    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

    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

    Request:

    Response (Success):

    Response (Error):

    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.

    Show users their linked accounts:

    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

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

    The user must unlink from the other account first.

    All link/unlink operations are recorded:

    For each linked social account:

    Field
    Description

    provider

    Provider name (google, github, etc.)

    provider_user_id

    Unique ID from provider

    email

    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

    • Configure Google - Set up providers

    • Enable MFA - Additional security

    • Audit Logging - Track account changes

    Overview

    Linking Accounts

    Automatic Linking

    <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"
    }

    Manual Linking

    Unlinking Accounts

    API Endpoint

    Safety Checks

    User Interface

    Account Settings Page

    Example UI

    JavaScript Handler

    Security Considerations

    Account Takeover Prevention

    Multiple Accounts Warning

    Audit Logging

    Data Stored

    Best Practices

    Next Steps

  • Scopes - Permitted access levels

  • Type
    Description
    Use Case

    Confidential

    Can securely store secrets

    Server-side apps

    Public

    Cannot store secrets

    1. Navigate to Admin Dashboard → OAuth Clients

    2. Click New Client

    3. Fill in the form:

    Field
    Description

    Name

    Display name for the client

    Type

    Confidential or Public

    Redirect URIs

    1. Click Create

    OAuth Clients

    After creation, you'll receive:

    • Client ID: abc123def456...

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

    Response:

    Do
    Don't

    Use exact URLs

    Use wildcards

    Use HTTPS in production

    Use HTTP in production

    Register all environments

    Configure which OAuth flows the client can use:

    Grant Type
    Value
    Use Case

    Authorization Code

    authorization_code

    Web apps

    PKCE

    authorization_code

    Method
    Description

    client_secret_basic

    HTTP Basic auth

    client_secret_post

    Secret in body

    none

    Assign allowed scopes:

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

    Store additional client information:

    After registration, test the authorization flow:

    • Configure Scopes - Create custom scopes

    • Rotate Secrets - Secret management

    • First OAuth Integration - Complete tutorial

    Overview

    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"

    Client Types

    Admin Dashboard

    Create Client

    Client Credentials

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

    API Registration

    Create Client via API

    Get Client Details

    Update Client

    Delete Client

    Redirect URI Configuration

    Best Practices

    Valid Examples

    Invalid Examples

    Grant Types

    Authentication Methods

    Scopes

    Client Metadata

    Testing Your Client

    Next Steps

    Scope
    Description
    Claims

    openid

    OpenID Connect

    sub

    profile

    User profile

    name, family_name, given_name, picture

    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

    1. Click Create

    Scopes
    1. Select the scope

    2. Modify fields

    3. Click Save

    1. Select the scope

    2. Click Delete

    3. Confirm deletion

    Response:

    Use colons to create hierarchies:

    Format: resource:action

    Scope
    Resource
    Action

    orders:read

    Orders

    Read

    orders:create

    Orders

    Include version if needed:

    When registering or updating a client:

    When users authorize a client, they see requested scopes:

    Authority validates that:

    1. Requested scopes exist

    2. Client is allowed to request them

    3. User consents to them

    Check scopes in access tokens:

    Set default scopes included in all authorizations:

    Or mark scopes as default in the admin dashboard.

    Define scopes that require other scopes:

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

    • Register Client - Create OAuth clients

    • Rotate Secrets - Secret management

    • Protect Your API - Scope enforcement

    Overview

    Default Scopes

    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"]
    }

    Admin Dashboard

    Create Scope

    Edit Scope

    Delete Scope

    Deleting a scope may break existing client integrations.

    API Management

    Create Scope

    List Scopes

    Update Scope

    Delete Scope

    Scope Naming Conventions

    Hierarchical Scopes

    Resource-Action Pattern

    API Versioning

    Assign Scopes to Clients

    Scope Consent

    Validating Scopes

    At Token Issuance

    In Your API

    Default Scopes

    Scope Dependencies

    Best Practices

    Do:

    • Use specific scopes (not just read/write)

    Avoid:

    • Overly broad scopes

    • Scopes that overlap

    Next Steps

    Configure LinkedIn - Enable LinkedIn Sign-In

  • Configure Apple - Enable Sign in with Apple

  • Manage Linked Accounts - Link/unlink social accounts

  • 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

    • Profile data - Access user profile information

    Provider
    Scopes
    Features

    Google

    email, profile, openid

    Email verification, profile picture

    GitHub

    user:email, read:user

    1. Create OAuth app with your chosen provider

    2. Configure credentials in Authority admin dashboard

    3. Enable provider in settings

    4. Add login button to your application

    See individual provider guides for detailed setup instructions.

    • Always use HTTPS in production

    • Store client secrets securely

    • Validate state parameter to prevent CSRF

    • Consider requiring email verification

    • Implement account linking carefully to prevent account takeover

    • Configure Google - Most common provider

    • Manage Linked Accounts - Account linking features

    Available Guides

    Configure Google
    Configure GitHub
    Configure Facebook

    Overview

    How It Works

    Supported Providers

    Quick Start

    Security Considerations

    Next Steps

    Go to Google Cloud Console

  • Create a new project or select existing one

  • Navigate to APIs & Services > Credentials

  • 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

    1. Log in to Authority admin dashboard

    2. Navigate to Settings > Social Login

    3. Enable Google OAuth

    4. Enter your credentials:

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

      • Client Secret: your-google-client-secret

    5. Save settings

    Add a Google sign-in button to your application:

    With forward URL (redirect after login):

    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

    4. A new user account is created (or existing account linked)

    5. You're redirected to your application

    Authority fetches the following from Google:

    Field
    Description

    sub

    Unique Google user ID

    email

    User's email address

    email_verified

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

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

    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)

    The client ID or secret is incorrect.

    Solution:

    • Verify credentials in Authority settings

    • Check for extra spaces or characters

    • Regenerate secret if needed

    Check Authority logs for errors. Common issues:

    • Email already exists with different provider

    • Database connection issues

    • Missing required scopes

    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

    4. Review permissions - Only request scopes you need

    5. Monitor usage - Check Google Console for suspicious activity

    • Configure GitHub - Add another provider

    • Manage Linked Accounts - Account linking

    • Enable MFA - Add extra security

    Prerequisites

    Step 1: Create Google OAuth App

    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

    Step 2: Configure Authority

    Using Admin Dashboard

    Using Environment Variables

    Step 3: Add Login Button

    Step 4: Test the Integration

    User Data Retrieved

    Troubleshooting

    "redirect_uri_mismatch" Error

    "Access blocked: App not verified"

    "Invalid client" Error

    User Not Created

    Security Best Practices

    Next Steps

    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();
    }

    MFA Flow

    User Setup

    Step 1: Navigate to Profile

    Step 2: Enable MFA

    Step 3: Scan QR Code

    Step 4: Verify Setup

    Backup Codes

    Admin Configuration

    Require MFA for Admins

    Require MFA for All Users

    MFA Grace Period

    Disabling MFA

    User Self-Service

    Admin Override

    API Integration

    Check MFA Status

    Require MFA in Authorization

    Recovery

    Lost Authenticator

    Lost Backup Codes

    Security Considerations

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

    • Regenerate backup codes periodically

    • Monitor audit logs for MFA disable events

    Troubleshooting

    Invalid TOTP Code

    QR Code Won't Scan

    Locked Out

    Next Steps

    Account Lockout
    Password Policies
    Audit Logging
    MFA Setup

    Rotate Secrets

    Manage and rotate OAuth client secrets for security.

    Why Rotate Secrets?

    • Secret may have been exposed

    • Regular security policy compliance

    • Employee departure

    • Security audit requirement

    1. Navigate to Admin Dashboard → OAuth Clients

    2. Select the client

    3. Click Rotate Secret

    4. Copy the new secret immediately

    Response:

    For production applications, use a two-step rotation:

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

    Update your application to use the new secret.

    If you must rotate immediately:

    Have the new secret ready to deploy.

    Update your application within seconds:

    Create a rotation script:

    Configure secrets to expire automatically:

    Monitor expiring secrets:

    All secret rotations are logged:

    Event
    Description

    Query the audit log:

    Set up alerts for secret events:

    If you lose a client secret:

    1. Log into admin dashboard

    2. Rotate to generate new secret

    3. Update application immediately

    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

    • - Create OAuth clients

    • - Access control

    • - Track rotations

    User Management

    Create and manage administrator accounts in Authority.

    Admin Capabilities

    Administrators can:

    • Manage OAuth clients

    • Create and edit users

    • Configure scopes

    • View audit logs

    • Modify system settings

    1. Navigate to Admin Dashboard → Users

    2. Click New User

    3. Fill in the form:

    Field
    Description
    1. Click Create

    1. Navigate to Users

    2. Select the user

    3. Click Edit

    4. Change Role to Administrator

    Create admin during initial setup:

    Role
    Permissions

    Require MFA for all admin accounts:

    Enforce stronger passwords for admins:

    Limit admin access by IP:

    Admin actions are logged:

    Event
    Description
    • - Session management

    • - Reset passwords

    • - Secure admin accounts

    Manage Sessions

    View and revoke user sessions in Authority.

    Overview

    Sessions represent active user logins. Each session tracks:

    • Device information

    • IP address

    • Login time

    • Last activity

    Users can see their sessions in the profile:

    1. Click profile name

    2. Select Security Settings

    3. View Active Sessions

    1. Find the session in the list

    2. Click Revoke

    3. Confirm revocation

    The session is immediately invalidated.

    Response:

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

    Variable
    Default
    Description

    Force users to have only one active session:

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

    End sessions after inactivity:

    Users are logged out after 30 minutes of inactivity.

    Each session captures:

    Field
    Description

    Email users about new logins:

    Email content:

    Alert on unusual patterns:

    • Login from new location

    • Multiple simultaneous sessions

    • Login outside business hours

    For security incidents:

    Revoking a session can optionally revoke associated tokens:

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

    Event
    Description
    • - Admin accounts

    • - Reset passwords

    • - Track sessions

    Password Reset

    Configure and manage password reset flows in Authority.

    User Self-Service Reset

    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

    5. Logs in with new password

    Users visit /forgot-password and enter their email.

    Authority sends an email with a secure reset link:

    Variable
    Default
    Description

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

    1. Navigate to Admin Dashboard → Users

    2. Select the user

    3. Click Reset Password

    4. Choose:

    Prevent abuse of reset endpoint:

    This allows 3 reset requests per hour per email.

    Reset tokens are:

    • Single-use

    • Time-limited

    • Cryptographically random

    • Invalidated on password change

    Don't reveal if email exists:

    Response is always:

    Password reset events are logged:

    Event
    Description

    Customize public/templates/reset-password.html:

    After successful reset, show:

    • Check spam folder

    • Verify email configuration

    • Check logs for sending errors

    • Confirm email address is correct

    • Request new reset link

    • Check PASSWORD_RESET_TTL setting

    • Verify password meets policy requirements

    • Check password history restrictions

    For custom applications:

    • - Admin accounts

    • - Session management

    • - Password requirements

    Customization

    Modify Authority's appearance with Jinja templates.

    Overview

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

    • Login / Registration

    • OAuth consent

    • Password reset

    • Admin dashboard

    • User profile

    Base layout (layout.html):

    Page template (signin.html):

    Variable
    Description
    Variable
    Description
    Variable
    Description

    Create a custom theme:

    In development, templates reload automatically. In production:

    • - Customize emails

    • - Logo and colors

    • - Full reference

    Email Templates

    Configure transactional emails sent by Authority.

    Email Types

    Authority sends these transactional emails:

    Email
    Trigger

    Welcome

    New user registration

    emails/base.html:

    emails/password-reset.html:

    emails/welcome.html:

    Variable
    Description
    Variable
    Description
    Variable
    Description
    Variable
    Description

    Configure default locale:

    • Check SMTP configuration

    • Verify credentials

    • Check spam filters

    • Review server logs

    • Use inline CSS

    • Test with email preview tools

    • Check image URLs are absolute

    • Use absolute URLs

    • Host images on accessible server

    • Check HTTPS requirements

    • - Customize pages

    • - Logo and colors

    • - Email settings

    Branding

    Customize Authority's visual identity for your organization.

    Logo

    Replace Logo

    Place your logo in the public directory:

    public/images/logo.png
    public/images/logo-dark.png  # For dark backgrounds
    public/images/favicon.ico

    Logo Requirements

    Type
    Size
    Format

    Logo

    200x50px

    Customize in public/css/styles.css:

    Show client logos on consent page:

    • - Template customization

    • - Email branding

    • - HTTPS setup

    API

    Complete reference for all Authority API endpoints.

    OAuth 2.0 Endpoints

    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>


    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


    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):


    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)


    POST /device/code

    Start device authorization flow (RFC 8628).

    Headers:

    Header
    Description

    Request Body:

    Parameter
    Required
    Description

    Response: 201 Created


    POST /device/token

    Poll for device authorization completion.

    Request Body:

    Parameter
    Required
    Description

    Response (Pending): 400 Bad Request

    Response (Success): 201 Created


    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.


    GET /oauth2/userinfo

    Get authenticated user's claims.

    Headers:

    Header
    Description

    Response: 200 OK


    GET /.well-known/openid-configuration

    OpenID Connect discovery document.

    Response: 200 OK

    Caching: public, max-age=3600


    GET /.well-known/jwks.json

    JSON Web Key Set for token verification.

    Response: 200 OK

    Caching: public, max-age=3600


    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


    GET /signin

    Display sign-in form.

    Query Parameters:

    Parameter
    Required
    Description

    Response: HTML sign-in form


    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


    POST /signout

    End user session.

    Response: 302 redirect to sign-in page


    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)


    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


    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


    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)


    POST /mfa/enable

    Enable MFA for account.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect

    Authentication Required: Yes (session)


    POST /mfa/verify

    Verify MFA code during login.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect to profile


    POST /mfa/disable

    Disable MFA for account.

    Request Body:

    Parameter
    Required
    Description

    Response: 302 redirect

    Authentication Required: Yes (session)


    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)


    GET /health_check

    Check server health.

    Response: 200 OK


    See for social authentication endpoints.


    • - Social authentication endpoints

    • - Error response reference

    • - Rate limiting details

    • - Grant type specifications

    Social Login

    API endpoints for social authentication (OAuth federation).

    Endpoints Overview

    Method
    Endpoint
    Description

    GET

    /auth/{provider}

    Initiate social login

    Provider
    Identifier

    Start the OAuth flow with a social provider.

    Parameter
    Type
    Description
    Parameter
    Type
    Required
    Description

    Redirects to the provider's authorization page.

    Status
    Error
    Description

    Handles the callback from the social provider after user authorization.

    Parameter
    Type
    Description
    Parameter
    Type
    Description

    On success, redirects to:

    • The forward_url if provided during initiation

    • The default post-login page otherwise

    Sets session cookie for authenticated user.

    Status
    Error
    Description

    Remove a linked social account from the authenticated user.

    Parameter
    Type
    Description

    Requires active session (session cookie).

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

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

    Status
    Error
    Description

    Each provider returns different user information.


    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.


    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


    Social login is configured via settings:

    Setting
    Description
    • - Setup guide

    • - Account linking

    • - All API endpoints

    Error Codes

    Reference for Authority API error responses.

    Error Response Format

    All errors follow this format:

    {
      "error": "error_code",
      "error_description": "Human-readable description",
      "error_uri": "https://docs.example.com/errors/error_code"
    }

    OAuth 2.0 Errors

    Authorization Endpoint Errors

    Error
    Description

    invalid_request

    Missing or invalid parameter

    Example:

    Error
    HTTP Status
    Description

    Example:

    Error
    HTTP Status
    Description
    Error
    HTTP Status
    Description

    Example:

    Error
    HTTP Status
    Description

    Example:

    Status
    Meaning
    Error
    HTTP Status
    Description
    Error
    HTTP Status
    Description

    Example with Details:

    Error
    HTTP Status
    Description
    Error
    HTTP Status
    Description

    Example:

    Error
    Cause
    Solution
    1. Check the error_description for details

    2. Verify all required parameters are included

    3. Ensure client credentials are correct

    4. Check token expiration

    • - Rate limiting details

    • - Endpoint reference

    • - Grant specifications

    Rate Limits

    Authority implements rate limiting to protect against abuse.

    Default Limits

    Endpoint
    Limit
    Window

    /authorize

    60

    1 minute

    Rate limit information is included in response headers:

    Header
    Description

    When rate limit is exceeded:

    Default rate limiting is per IP address:

    Rate limit by OAuth client:

    Rate limit by authenticated user:

    Variable
    Default
    Description

    Exclude IPs from rate limiting:

    Configure different limits per client:

    1. Check headers - Monitor X-RateLimit-Remaining

    2. Implement backoff - Use exponential backoff on 429

    3. Cache tokens - Reduce token requests

    4. Batch requests

    Monitor rate limit metrics:

    • Rate of 429 responses

    • Clients hitting limits frequently

    • Unusual traffic patterns

    For distributed deployments, use Redis:

    This ensures consistent rate limiting across multiple Authority instances.

    • - Endpoint reference

    • - Error handling

    • - Redis setup

    OAuth 2.0

    Complete reference for OAuth 2.0 grant types supported by Authority.

    Grant Type Overview

    Available Grant Types

    Grant Type Comparison

    Grant Type
    Use Case
    Client Type
    Security

    Web apps with backend

    Endpoint
    Method
    Description

    Use Authorization Code with a backend:

    Use Authorization Code + PKCE:

    Use Client Credentials:

    Use Device Code:

    Authority implements these RFCs:

    • - OAuth 2.0 Framework

    • - Bearer Token Usage

    • - PKCE

    • - Token Introspection

    • - Standard web app flow

    • - Mobile/SPA flow

    • - Machine-to-machine

    • - IoT and CLI devices

    Authorization Code

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

    Overview

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

    Flow Diagram

    Authorization Request

    GET /authorize

    Redirect the user to the authorization endpoint.

    Parameters

    Parameter
    Required
    Description

    User is redirected to redirect_uri with:

    POST /token

    Exchange the authorization code for tokens.

    Header
    Value
    Parameter
    Required
    Description
    1. Always validate state - Prevents CSRF attacks

    2. Use HTTPS - Never transmit tokens over HTTP

    3. Store secrets securely - Never expose client_secret

    4. Validate redirect_uri - Use exact match

    • - For public clients

    • - Token renewal

    • - Response format

    Authorization Code + PKCE

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

    Overview

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

    When to Use

    • Mobile applications

    • Single-page applications (SPAs)

    • Desktop applications

    • Any client that cannot securely store secrets

    Parameter
    Description

    GET /authorize

    Parameter
    Required
    Description

    POST /token

    Parameter
    Required
    Description

    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

    • - Confidential clients

    • - Token renewal

    • - Response format

    Client Credentials

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

    Overview

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

    Use Cases

    • Background jobs

    • Microservice communication

    • API integrations

    • Scheduled tasks

    • System maintenance scripts

    POST /token

    Header
    Value
    Parameter
    Required
    Description

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

    Clients can only request scopes they're authorized for:

    Aspect
    Client Credentials
    Authorization Code
    • - Token renewal

    • - Response format

    • - Secret management

    Device Flow

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

    Overview

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

    Use Cases

    • Smart TVs

    • Media consoles

    • Picture frames

    • Printers

    • CLI applications

    • IoT devices

    POST /device

    Parameter
    Required
    Description
    Field
    Description

    Show the user:

    Or display a QR code linking to verification_uri_complete.

    POST /token

    Poll the token endpoint while waiting for user authorization.

    Parameter
    Required
    Description

    Authorization Pending:

    Slow Down (polling too fast):

    Success:

    Access Denied:

    Expired Token:

    The activation page shows:

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

    • - Response format

    • - Token renewal

    • - Error handling

    Refresh Tokens

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

    Overview

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

    Token Lifecycle

    Token Response

    When you receive tokens, note the expiration:

    {
      "access_token": "eyJhbGciOiJSUzI1NiIs...",
      "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
      "token_type": "Bearer",
      "expires_in": 3600
    }
    Field
    Description

    access_token

    Short-lived token for API access

    POST /token

    Header
    Value
    Parameter
    Required
    Description

    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.

    Refresh before expiration:

    Refresh when access token is rejected:

    When this happens, the user must re-authenticate.

    Request fewer scopes when refreshing:

    You cannot request more scopes than originally granted.

    Variable
    Default
    Description

    To receive refresh tokens, request the offline_access scope:

    Revoke refresh tokens when:

    • User logs out

    • User changes password

    • Security concern detected

    • - Response format

    • - Error handling

    • - Conceptual overview

    Token Response

    Reference for OAuth 2.0 token endpoint responses.

    Successful Response

    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..."
    }

    Response Fields

    Field
    Type
    Description

    access_token

    String

    The access token

    Authority issues JWTs as access tokens:

    Header:

    Payload:

    Claim
    Description

    No refresh token or ID token for client credentials.

    New refresh token issued (rotation).

    Use the access token in API requests:

    See for all error types.

    1. Parse the JWT

    2. Verify signature using JWKS

    3. Check issuer matches Authority URL

    4. Check audience matches your client ID

    For opaque tokens or real-time validation:

    Response:

    HTTP headers prevent caching:

    Never cache token responses.

    • - Token renewal

    • - Error handling

    • - Conceptual overview

    Legacy Flows

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

    • 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

    Password Grant

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

    Acceptable:

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

    • Migration from legacy systems

    • Trusted internal tools

    OpenID Connect

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

    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

    OpenID Connect Discovery allows clients to automatically configure themselves.

    GET /.well-known/openid-configuration

    Returns a JSON document with all provider configuration.

    Field
    Description

    JWKS

    The JWKS endpoint provides public keys for verifying JWT signatures.

    GET /.well-known/jwks.json

    Returns the public keys used to sign tokens.

    Property
    Description

    UserInfo

    The UserInfo endpoint returns claims about the authenticated user.

    GET /userinfo POST /userinfo

    Both methods are supported.

    Claims returned depend on requested scopes:

    Claim

    ID Tokens

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

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

    ID tokens are JWTs with three parts:

    Claim
    Description

    Configuration

    Complete reference for all Authority configuration options.

    Variable
    Default
    Description

    Security Settings

    Configuration options for Authority security features.

    Variable
    Default
    Description

    Token Settings

    Configuration options for OAuth 2.0 tokens.

    Variable
    Default
    Description

    Glossary

    OAuth 2.0 and OpenID Connect terminology.

    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.

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

    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.).

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

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

    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.

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

    Understanding Authority

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

    • - Fundamentals of the OAuth 2.0 protocol

    • - Identity layer on top of OAuth 2.0

    • - Decision guide for selecting the right flow

    Architecture

    Understanding how Authority is built and organized.

    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

    OAuth 2.0 Concepts

    Understanding the fundamentals of OAuth 2.0.

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

    Without OAuth, applications would need:

    • Direct access to user credentials

    • Full access to all resources

    OpenID Connect Concepts

    Understanding identity and authentication with OpenID Connect.

    Aspect
    OAuth 2.0
    OpenID Connect

    Security Model

    Understanding Authority's security architecture and design principles.

    Authority implements multiple security layers:

    Passwords are:

    1. Hashed - Using bcrypt with cost factor 12

    2. Salted - Unique salt per password

    Token Lifecycle

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

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

    • One-time use - Cannot be reused

    • Short-lived - Minimizes interception window

    • Client-bound - Tied to specific client

    Choosing Grant Types

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

    Scenario
    Grant Type
    Security

    Why Authority?

    Understanding the benefits and use cases for Authority.

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

    • Complete OAuth 2.0 implementation

    • OpenID Connect identity layer

    • Enterprise security features

    Primary email (if public or authorized)

    name

    Display name

    avatar_url

    Profile picture URL

    Full name

    picture

    Profile picture URL

    Email verification status

    name

    Full name

    given_name

    First name

    family_name

    Last name

    picture

    Profile picture URL

  • Click Save

  • Shown when creating the key

    Private Key

    Contents of the .p8 file

    Always true for Apple

    name

    Name (only on first auth)

    Email from provider

    name

    Name from provider

    avatar_url

    Profile picture URL

    access_token

    Provider access token (encrypted)

    refresh_token

    Provider refresh token (encrypted)

    token_expires_at

    Token expiration time

    Whether email is verified

    name

    Full name

    given_name

    First name

    family_name

    Last name

    picture

    Profile picture URL

    Regularly review logs for anomalies

    user.logout

    User logout

    user.created

    New user registration

    user.updated

    Profile update

    user.deleted

    User deletion

    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

    Event type

    actor_id

    User or client who performed action

    actor_type

    user, client, or system

    resource_type

    Type of resource affected

    resource_id

    ID of affected resource

    ip_address

    Client IP address

    user_agent

    Browser/client information

    metadata

    Additional event-specific data

    Mobile apps, SPAs

    Callback URLs (one per line)

    Scopes

    Allowed scopes

    Grant Types

    Enabled OAuth flows

    Use localhost in production

    Mobile/SPA

    Client Credentials

    client_credentials

    Service-to-service

    Refresh Token

    refresh_token

    Token renewal

    Device Code

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

    IoT/CLI

    Public client (no secret)

    Document what each scope grants
  • Use hierarchical naming

  • Include scopes in API documentation

  • Changing scope meaning after deployment

    email

    Email address

    email, email_verified

    address

    Postal address

    address

    phone

    Phone number

    phone_number, phone_number_verified

    offline_access

    Refresh tokens

    -

    Include in all authorizations

    Create

    products:list

    Products

    List

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

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

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

    • Authorization Code - Web applications

    • Client Credentials - Machine-to-machine

    • Device Code - IoT devices, CLIs

    • Refresh Token - Token renewal

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

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

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

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

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

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

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

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

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

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

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

    An endpoint that returns claims about the authenticated user.

    The mechanism to automatically find authorization server endpoints and capabilities.

    A set of public keys used to verify token signatures.

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

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

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

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

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

    The process of invalidating a token before its natural expiration.

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

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

    Temporarily blocking access after multiple failed authentication attempts.

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

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

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

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

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

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

    The timestamp after which the token should not be accepted.

    The OAuth 2.0 Authorization Framework specification.

    OAuth 2.0 Bearer Token Usage specification.

    Proof Key for Code Exchange (PKCE) specification.

    Device Authorization Grant specification.

    The specification for identity layer on top of OAuth 2.0.

    Core Concepts

    Resource Owner

    Resource Server

    Client

    Authorization Server

    Token Types

    Access Token

    Refresh Token

    ID Token

    Authorization Code

    Device Code

    OAuth Concepts

    Grant Type

    Scope

    Redirect URI

    State

    PKCE (Proof Key for Code Exchange)

    Code Verifier

    Code Challenge

    Client Types

    Confidential Client

    Public Client

    First-Party Client

    Third-Party Client

    OpenID Connect

    Claims

    UserInfo Endpoint

    Discovery

    JWKS (JSON Web Key Set)

    Nonce

    Authentication

    MFA (Multi-Factor Authentication)

    TOTP (Time-based One-Time Password)

    Session

    Security

    Token Introspection

    Token Revocation

    Consent

    Audit Log

    Account Lockout

    JWT (JSON Web Token)

    Header

    Payload

    Signature

    Issuer (iss)

    Subject (sub)

    Audience (aud)

    Expiration (exp)

    Standards

    RFC 6749

    RFC 6750

    RFC 7636

    RFC 8628

    OpenID Connect Core 1.0

    Architecture - How Authority is built and organized

  • Security Model - Security architecture and design principles

  • Token Lifecycle - How tokens are issued, used, and revoked

    • Why Authority? - Benefits and use cases

    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

    • Evaluate trade-offs between approaches

    After understanding the concepts, explore:

    • Tutorials - Learn by building

    • How-To Guides - Solve specific problems

    • Reference - Technical specifications

    Understanding OAuth 2.0

    Architecture

    OAuth 2.0 Concepts
    OpenID Connect Concepts
    Choosing Grant Types

    About Authority

    How to Use This Section

    Further Reading

    Update your application configuration

  • The old secret is immediately invalidated

  • client.secret_rotated

    Secret was rotated

    client.secret_expired

    Secret expired

    Rotation Methods

    Method 1: Admin Dashboard

    Method 2: API

    Zero-Downtime Rotation

    Step 1: Add New Secret

    Step 2: Update Application

    Step 3: Remove Old Secret

    Rotation Without Dual Secrets

    1. Prepare New Configuration

    2. Rotate Secret

    3. Deploy Immediately

    Automation

    Scheduled Rotation

    Cron Schedule

    Secret Expiration

    Audit Trail

    Notification

    Recovery

    Lost Secret

    Compromised Secret

    Best Practices

    Do:

    • Rotate secrets regularly (quarterly minimum)

    • Automate rotation process

    • Use secrets management (Vault, AWS Secrets Manager)

    • Monitor for secret exposure

    Avoid:

    • Storing secrets in code

    • Sharing secrets via email/chat

    • Long-lived secrets without rotation

    • Manual rotation in production

    Next Steps

    Register Client
    Configure Scopes
    Audit Logging

    Role

    Select Administrator

    Click Save

    client.created

    Client created by admin

    settings.changed

    Settings modified

    Email

    Admin email address

    Name

    Display name

    Password

    user

    Profile management, OAuth authorizations

    admin

    Full system access

    super_admin

    admin.login

    Admin login

    admin.created

    New admin created

    admin.role_changed

    Admin Dashboard

    Create Admin User

    Promote Existing User

    Command Line

    Create Admin via CLI

    Using Database Seed

    API

    Create Admin via API

    Update User Role

    Admin Roles

    Role Hierarchy

    Security Requirements

    MFA for Admins

    Admin Password Policy

    IP Restrictions

    Audit Trail

    Best Practices

    Do:

    • Use individual accounts (not shared)

    • Enable MFA for all admins

    • Regularly review admin list

    • Limit super_admin count

    Avoid:

    • Shared admin credentials

    • Admin accounts without MFA

    • Unnecessary admin access

    • Using admin accounts for daily work

    Removing Admin Access

    Demote to User

    Deactivate Admin

    Next Steps

    Manage Sessions
    Password Reset
    Enable MFA
    User Management

    Initial password

    Can create other admins

    Role modified

    SINGLE_SESSION

    false

    Allow only one active session

    location

    Approximate location (if enabled)

    created_at

    Login timestamp

    last_activity

    Last request timestamp

    session.expired

    Session timed out

    SESSION_DURATION_DAYS

    7

    Maximum session lifetime

    IDLE_TIMEOUT_MINUTES

    30

    ip_address

    Client IP at login

    user_agent

    Browser/app information

    device

    session.created

    New login

    session.refreshed

    Session activity

    session.revoked

    User Self-Service

    View Active Sessions

    Revoke Session

    Admin Management

    View User Sessions

    Revoke Specific Session

    Revoke All User Sessions

    Session Settings

    Configuration

    Single Session Mode

    Idle Timeout

    Session Information

    Security Alerts

    Notify on New Session

    Suspicious Session Detection

    Bulk Session Management

    Revoke All Sessions (System-Wide)

    Revoke by Criteria

    Session in OAuth Flow

    Token and Session Relationship

    SSO Session

    Monitoring

    Active Session Count

    Session Audit Events

    Next Steps

    Create Admin
    Password Reset
    Audit Logging
    User Profile

    Timeout after inactivity

    Parsed device type

    Session ended

    Send Reset Email - User receives email

  • Set Password - Admin sets new password

  • password.reset_failed

    Reset attempt failed

    PASSWORD_RESET_TTL

    3600

    Reset token lifetime (seconds)

    PASSWORD_RESET_EMAIL_SUBJECT

    Reset your password

    password.reset_requested

    Reset email requested

    password.reset_sent

    Reset email sent

    password.reset_completed

    Request Reset

    Reset Email

    Configuration

    Environment Variables

    Email Template

    Admin Password Reset

    Reset via Dashboard

    Reset via API

    Send Reset Email

    Set Password Directly

    Setting passwords directly should notify the user.

    Security Considerations

    Rate Limiting

    Token Security

    Email Enumeration Protection

    Audit Trail

    Customization

    Reset Page Styling

    Success Page

    Troubleshooting

    Email Not Received

    Token Expired

    Password Not Accepted

    Integration

    Custom Reset Flow

    Next Steps

    Create Admin
    Manage Sessions
    Password Policies

    Email subject

    Password was reset

    user.name

    User's name

    user.email

    User's email

    login_time

    Login timestamp

    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

    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

    Template Location

    Template Structure

    Base Template

    Password Reset Email

    Welcome Email

    Available Variables

    All Emails

    Password Reset

    Email Verification

    New Login Alert

    Configuration

    SMTP Settings

    Email Settings

    Testing Emails

    Preview in Browser

    Send Test Email

    HTML Email Best Practices

    Email HTML is limited:

    • Use inline CSS

    • Use tables for layout

    • Avoid JavaScript

    • Test across email clients

    Compatible Styles

    Localization

    Multiple Languages

    Troubleshooting

    Emails Not Sending

    Broken Layout

    Images Not Loading

    Next Steps

    UI Templates
    Branding
    Environment Variables

    Company address

    Client IP

    PNG, SVG

    Logo (dark)

    200x50px

    PNG, SVG

    Favicon

    32x32px

    ICO, PNG

    Apple Touch

    180x180px

    PNG

    Template Usage

    Colors

    Primary Colors

    Dark Theme (Default)

    Light Theme

    Application Name

    Configuration

    Template Usage

    Custom Fonts

    Google Fonts

    Self-Hosted Fonts

    Login Page

    Background Image

    Login Card

    Email Branding

    Email Header

    Email Footer

    OAuth Consent Page

    Client Branding

    Scope Icons

    Configuration Reference

    Best Practices

    Do:

    • Use consistent brand colors

    • Maintain good contrast ratios

    • Test on mobile devices

    • Include alt text for images

    Avoid:

    • Low contrast text

    • Very small fonts

    • Overly complex layouts

    • Slow-loading images

    Next Steps

    UI Templates
    Email Templates
    SSL Certificates

    Yes

    Space-separated scopes

    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

    redirect_uri

    Yes

    Callback URL

    scope

    Yes

    Space-separated scopes

    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

    redirect_uri

    Yes

    Same as authorization request

    code_verifier

    For PKCE

    Original code verifier

    password

    Yes

    User's password

    scope

    Optional

    Requested scopes

    code

    Yes

    Device code from /device/code

    forward_url

    Optional

    Base64-encoded redirect URL

    Active sessions

    response_type

    Yes

    Must be code

    client_id

    Yes

    The client identifier

    redirect_uri

    Yes

    Callback URL (must match registered URI)

    response_type

    Yes

    Must be code

    client_id

    Yes

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    authorization_code

    code

    Yes

    grant_type

    Yes

    refresh_token

    refresh_token

    Yes

    grant_type

    Yes

    client_credentials

    scope

    Optional

    grant_type

    Yes

    password

    username

    Yes

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    token

    Yes

    Token to introspect

    token_type_hint

    Optional

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    token

    Yes

    Token to revoke

    token_type_hint

    Optional

    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

    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

    email

    Yes

    User's email address

    email

    Yes

    User's email address

    token

    Yes

    Reset token from email

    password

    Yes

    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

    Token Endpoint

    Token Introspection

    Token Revocation

    Device Authorization

    Device Token Polling

    Device Activation

    OpenID Connect Endpoints

    UserInfo

    Discovery

    JWKS

    Dynamic Client Registration

    Register Client

    Authentication Endpoints

    Sign In Form

    Sign In

    Sign Out

    Account Endpoints

    Forgot Password

    Password Reset

    Email Verification

    MFA Endpoints

    MFA Setup

    Enable MFA

    Verify MFA

    Disable MFA

    User Profile

    View Profile

    Health Check

    Social Login

    Next Steps

    Social Login API
    Social Login
    Error Codes
    Rate Limits
    OAuth 2.0 Flows

    scope

    The client identifier

    Authorization code

    Refresh token

    Requested scopes

    User's email

    access_token or refresh_token

    access_token or refresh_token

    Client identifier

    User's password

    New password

    LinkedIn

    linkedin

    Apple

    apple

    error

    string

    Error code if authorization failed

    error_description

    string

    Human-readable error message

    400

    invalid_code

    Authorization code invalid or expired

    500

    provider_error

    Error communicating with provider

    400

    cannot_unlink

    Would leave account without login method

    401

    unauthorized

    Not authenticated

    Authority validates state matches stored value
    User redirected to application

    Google OAuth client secret

    GITHUB_OAUTH_ENABLED

    Enable GitHub provider

    GITHUB_CLIENT_ID

    GitHub OAuth client ID

    GITHUB_CLIENT_SECRET

    GitHub OAuth client secret

    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)

    GET

    /auth/{provider}/callback

    OAuth callback handler

    POST

    /auth/{provider}/unlink

    Unlink social account

    Google

    google

    GitHub

    github

    Facebook

    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

    string

    Provider identifier

    code

    string

    Authorization code from provider

    state

    string

    400

    invalid_state

    State parameter invalid or expired

    400

    authorization_denied

    provider

    string

    Provider identifier to unlink

    400

    invalid_provider

    Provider not recognized

    400

    not_linked

    GOOGLE_OAUTH_ENABLED

    Enable Google provider

    GOOGLE_CLIENT_ID

    Google OAuth client ID

    Supported Providers

    Initiate Social Login

    Path Parameters

    Query Parameters

    Response

    Example

    Errors

    OAuth Callback

    Path Parameters

    Query Parameters

    Response

    Errors

    Unlink Social Account

    Path Parameters

    Authentication

    Response

    Errors

    Example

    User Data from Providers

    Google

    GitHub

    Facebook

    LinkedIn

    Apple

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

    State Parameter

    Session Handling

    Configuration Settings

    Next Steps

    Configure Google
    Manage Linked Accounts
    Endpoints

    facebook

    Provider not enabled in settings

    CSRF protection state parameter

    User denied authorization

    User doesn't have this provider linked

    GOOGLE_CLIENT_SECRET

    invalid_grant

    400

    Invalid authorization code or refresh token

    unauthorized_client

    400

    Client not authorized for grant type

    unsupported_grant_type

    400

    Grant type not supported

    invalid_scope

    400

    Invalid scope requested

    access_denied

    401

    User denied authorization

    expired_token

    400

    Device code expired

    400

    Bad Request - Invalid parameters

    401

    Unauthorized - Authentication required

    403

    Forbidden - Insufficient permissions

    404

    Not Found - Resource doesn't exist

    429

    Too Many Requests - Rate limited

    500

    Server Error - Internal error

    503

    Service Unavailable - Temporarily down

    account_locked

    403

    Account is locked

    mfa_required

    403

    MFA verification required

    invalid_parameter

    400

    Parameter format invalid

    gone

    410

    Resource no longer available

    invalid_redirect_uri

    Mismatched redirect URI

    Use exact registered URI

    invalid_scope

    Unknown scope

    Use only registered scopes

    invalid_token

    Expired token

    Refresh or re-authenticate

    Review server logs for more context

    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

    temporarily_unavailable

    Server is temporarily unavailable

    invalid_request

    400

    Missing required parameter

    invalid_client

    401

    invalid_request

    400

    Missing token parameter

    invalid_client

    401

    authorization_pending

    400

    User hasn't completed authorization

    slow_down

    400

    invalid_token

    401

    Token is invalid or expired

    insufficient_scope

    403

    200

    Success

    201

    Created

    302

    authentication_required

    401

    No valid authentication provided

    invalid_credentials

    401

    validation_error

    400

    Request validation failed

    missing_parameter

    400

    not_found

    404

    Resource not found

    already_exists

    409

    rate_limit_exceeded

    429

    Too many requests

    invalid_client

    Wrong client credentials

    Verify client_id and client_secret

    invalid_grant

    Expired or used code

    Token Endpoint Errors

    Token Introspection Errors

    Device Flow Errors

    OpenID Connect Errors

    UserInfo Errors

    HTTP Status Codes

    API Errors

    Authentication Errors

    Validation Errors

    Resource Errors

    Rate Limiting Errors

    Troubleshooting

    Common Issues

    Debug Tips

    Next Steps

    Rate Limits
    API Endpoints
    OAuth 2.0 Flows

    Client authentication failed

    Client authentication failed

    Polling too frequently

    Token lacks required scope

    Redirect

    Username or password incorrect

    Required parameter missing

    Resource already exists

    Request new authorization code

    RATE_LIMIT_WINDOW

    60

    Window in seconds

    RATE_LIMIT_MAX

    60

    Max requests per window

    - Combine when possible

    /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

    RATE_LIMIT_ENABLED

    true

    Enable rate limiting

    RATE_LIMIT_BY

    ip

    Response Headers

    Rate Limit Exceeded

    Rate Limit Types

    Per IP Address

    Per Client

    Per User

    Configuration

    Environment Variables

    Per-Endpoint Configuration

    Whitelist

    Client-Specific Limits

    Best Practices

    Client Implementation

    Handling Rate Limits

    Monitoring

    Redis-Based Rate Limiting

    Next Steps

    API Endpoints
    Error Codes
    Redis Caching

    Unix timestamp when limit resets

    Rate limit key

    exp

    Expiration time

    iat

    Issued at time

    scope

    Granted scopes

    client_id

    Client identifier

    Check expiration is in the future

  • Check scope includes required permissions

  • token_type

    String

    Always Bearer

    expires_in

    Integer

    Token lifetime in seconds

    refresh_token

    String

    Token for renewal (optional)

    scope

    String

    Granted scopes (space-separated)

    id_token

    String

    OIDC ID token (when openid scope)

    iss

    Issuer (Authority URL)

    sub

    Subject (user ID)

    aud

    Access Token Format

    Decoded JWT

    JWT Claims

    Token Types by Grant

    Authorization Code

    Client Credentials

    Refresh Token

    Bearer Token Usage

    Authorization Header (Recommended)

    Query Parameter (Not Recommended)

    Query parameter usage exposes tokens in logs and browser history.

    Error Response

    Token Validation

    JWT Validation Steps

    Example Validation

    Token Introspection

    Caching

    Next Steps

    Error Codes
    Refresh Tokens
    Error Codes
    Token Lifecycle

    Audience (client ID)

    PKCE is better - Authorization code + PKCE is now preferred

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

    Replace implicit grant with authorization code + PKCE:

    Before (Implicit):

    After (PKCE):

    • Authorization Code + PKCE - Recommended replacement

    • OAuth 2.0 Reference - All grant types

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

    Overview

    Why It's Deprecated

    Flow

    Authorization Request

    Response

    Migration to PKCE

    Next Steps

    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)

    JWTs include the key ID in the header:

    Match this kid with the key in JWKS.

    JWKS should be cached to avoid excessive requests:

    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

    • ID Tokens - Token structure

    • UserInfo - User claims

    • Discovery - Provider configuration

    JWKS Endpoint

    Response

    Key Properties

    kty

    Token Verification

    JavaScript

    Python

    Go

    Key Matching

    Caching

    Key Rotation

    Manual Verification

    Next Steps

    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}
    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
    }
    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
    }
    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'
      })
    });
    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
    <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
    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"
    }
    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"
    }
    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
    }
    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
    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
    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
      })
    });
    {
      "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'));
    }

    redirect_uri

    Callback URL

    state

    CSRF state parameter

    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 %}

    forward_url

    Redirect URL after login

    error

    Error message

    username

    client_name

    OAuth client name

    client_id

    Client identifier

    scopes

    user_code

    Pre-filled code

    audience

    Client name

    error

    :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

    Template Structure

    UI Components

    Template Syntax

    Variables

    Conditionals

    Loops

    Template Inheritance

    Available Variables

    Sign In Page

    Authorization Page

    Device Activation

    Styling

    CSS Variables

    Custom Theme

    Screenshots

    Landing Page

    Sign In

    Admin Dashboard

    Configuration

    Custom Template Path

    Reload Templates

    Best Practices

    Do:

    • Keep custom templates in version control

    • Test on multiple screen sizes

    • Maintain accessibility (ARIA, contrast)

    • Preserve security features (CSRF tokens)

    Avoid:

    • Removing hidden form fields

    • Modifying form action URLs

    • Disabling client-side validation

    • Adding inline JavaScript

    Next Steps

    Email Templates
    Branding
    Landing Page
    Sign In
    Admin Dashboard

    Previously entered username

    Requested scopes

    Error message

    /token/introspect

    POST

    Validate a token

    /token/revoke

    POST

    Revoke a token

    /device

    POST

    Start device flow

    RFC 7009 - Token Revocation

  • RFC 8628 - Device Authorization

  • Confidential

    High

    Authorization Code + PKCE

    Mobile apps, SPAs

    Public

    High

    Client Credentials

    Server-to-server

    Confidential

    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

    /authorize

    GET

    Start authorization flow

    /token

    POST

    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

    Endpoints

    Recommended Flows

    For Web Applications

    For Mobile Apps / SPAs

    For Backend Services

    For CLI / IoT Devices

    Security Recommendations

    Do:

    • Always use HTTPS

    • Validate state parameter

    • Use PKCE for public clients

    • Store tokens securely

    • Validate tokens server-side

    Avoid:

    • Implicit grant (deprecated)

    • Password grant for third-party apps

    • Long-lived access tokens

    • Storing tokens in localStorage

    Standards

    Next Steps

    RFC 6749
    RFC 6750
    RFC 7636
    RFC 7662
    Authorization Code
    Authorization Code + PKCE
    Client Credentials
    Device Flow
    Authorization Code

    Exchange code for tokens

    Yes

    Space-separated scopes

    state

    Recommended

    CSRF protection token

    redirect_uri

    Yes

    Same as authorization request

    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

    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

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    Must be authorization_code

    code

    Yes

    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')

    Example

    Response

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

    Token Request

    Headers

    Parameters

    Example

    Response

    Complete Example

    Node.js

    Python

    Security Considerations

    Next Steps

    Authorization Code + PKCE
    Refresh Tokens
    Token Response

    scope

    The authorization code

    redirect_uri

    Yes

    Callback URL

    scope

    Yes

    Space-separated scopes

    state

    Recommended

    CSRF protection

    code_challenge

    Yes

    Base64URL(SHA256(verifier))

    code_challenge_method

    Yes

    S256

    redirect_uri

    Yes

    Same as authorization

    client_id

    Yes

    Client identifier

    code_verifier

    Yes

    Original code verifier

    code_verifier

    Random string 43-128 characters

    code_challenge

    Base64URL(SHA256(code_verifier))

    code_challenge_method

    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()

    response_type

    Yes

    Must be code

    client_id

    Yes

    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

    grant_type

    Yes

    authorization_code

    code

    Yes

    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;
      }
    }

    Flow Diagram

    PKCE Parameters

    Generate PKCE Values

    JavaScript

    Python

    Authorization Request

    Parameters

    Example

    Token Request

    Parameters

    Example

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

    Complete Example

    React SPA

    Mobile (React Native)

    Security Benefits

    Best Practices

    • Always use S256 method (not plain)

    • Generate cryptographically random verifiers

    • Store verifier securely during flow

    • Clear verifier after token exchange

    Next Steps

    Authorization Code
    Refresh Tokens
    Token Response

    S256 (recommended) or plain

    Your client identifier

    Authorization code

    Use case

    Service-to-service

    User authentication

    Client type

    Confidential only

    Both

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    Must be client_credentials

    scope

    Optional

    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

    User involved

    No

    Yes

    Refresh token

    No

    Flow Diagram

    Token Request

    Headers

    Parameters

    Example

    Response

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

    Authentication Methods

    HTTP Basic Authentication (Recommended)

    POST Body

    Complete Examples

    Node.js

    Python

    curl

    Token Caching

    Scopes

    Security Considerations

    • Protect credentials - Store client secret securely

    • Use short token lifetimes - Minimize exposure window

    • Rotate secrets regularly - Limit compromise impact

    • Limit scopes - Request only needed permissions

    Differences from Other Grants

    Next Steps

    Refresh Tokens
    Token Response
    Rotate Secrets

    Space-separated scopes

    Yes

    verification_uri_complete

    URL with code pre-filled

    expires_in

    Seconds until codes expire

    interval

    Minimum polling interval

    client_id

    Yes

    Client identifier

    client_id

    Yes

    Client identifier

    scope

    Optional

    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
    }

    device_code

    Code for token polling (keep secret)

    user_code

    Code user enters (6-8 characters)

    verification_uri

    To sign in, visit:
    
      https://auth.example.com/activate
    
    And enter the code:
    
      WDJB-MJHT
    
    Waiting for authorization...

    grant_type

    Yes

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

    device_code

    Yes

    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]

    Flow Diagram

    Device Authorization Request

    Parameters

    Example

    Response

    Display to User

    Token Polling

    Parameters

    Example

    Responses

    Complete Example

    CLI Application (Node.js)

    Python CLI

    User Experience

    Security Considerations

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

    • Respect interval - Don't poll faster than specified

    • Handle expiration - Start over if codes expire

    • Use HTTPS - All communication must be encrypted

    Next Steps

    Token Response
    Refresh Tokens
    Error Codes

    Requested scopes

    URL user visits

    Device code from initial response

    scope

    Optional

    Request subset of original scopes

    REFRESH_TOKEN_ROTATION

    true

    Rotate refresh tokens

    refresh_token

    Long-lived token for renewal

    expires_in

    Access token lifetime in seconds

    Content-Type

    application/x-www-form-urlencoded

    Authorization

    Basic {base64(client_id:client_secret)}

    grant_type

    Yes

    Must be refresh_token

    refresh_token

    Yes

    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"

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    REFRESH_TOKEN_TTL

    2592000

    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

    Refresh Token Request

    Headers

    Parameters

    Example

    Response

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

    Token Rotation

    Implementation

    Proactive Refresh

    Reactive Refresh

    Error Handling

    Invalid Refresh Token

    Handling Errors

    Scope Reduction

    Configuration

    Token Lifetimes

    Offline Access

    Security Best Practices

    Do:

    • Store refresh tokens securely

    • Use token rotation

    • Refresh proactively before expiry

    • Handle refresh failures gracefully

    Avoid:

    • Storing refresh tokens in localStorage

    • Sharing refresh tokens between tabs

    • Ignoring rotation (using old tokens)

    • Long refresh token lifetimes without rotation

    Revocation

    Next Steps

    Token Response
    Error Codes
    Token Lifecycle

    The refresh token

    Refresh token lifetime (30 days)

    Never use for:
    • Third-party applications

    • Public-facing apps

    • Any app you don't fully trust

    POST /token

    Parameter
    Required
    Description

    grant_type

    Yes

    Must be password

    username

    Yes

    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

    4. MFA bypass - May skip multi-factor authentication

    To enable password grant (not recommended):

    Replace password grant with proper OAuth flows:

    Use Authorization Code + PKCE:

    Use Authorization Code:

    • Authorization Code + PKCE - Recommended for mobile/SPA

    • Authorization Code - Recommended for web apps

    • Client Credentials - For machine-to-machine

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

    Overview

    When to Use

    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&...`;

    Token Request

    Parameters

    Example

    Response

    Security Risks

    Configuration

    Migration Path

    Mobile Apps

    Web Applications

    Next Steps

  • Discovery - Automatic configuration

  • Endpoint
    Description

    /.well-known/openid-configuration

    Discovery document

    /.well-known/jwks.json

    Public keys for verification

    /userinfo

    Request OIDC scopes to get identity information:

    Scope
    Claims

    openid

    sub (required for OIDC)

    profile

    name, family_name, given_name, picture, etc.

    email

    The ID token is a JWT containing user identity:

    See ID Tokens for full specification.

    See UserInfo for details.

    Automatic configuration via:

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

    See Discovery for full specification.

    Aspect
    OAuth 2.0
    OpenID Connect

    Purpose

    Authorization

    Authentication

    Token

    Access token

    Authority implements:

    • OpenID Connect Core 1.0

    • OpenID Connect Discovery 1.0

    • OpenID Connect Dynamic Client Registration 1.0

    • Discovery - Automatic configuration

    • JWKS - Token verification keys

    • UserInfo - User claims endpoint

    • ID Tokens - Token specification

    Overview

    {
      "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());

    Endpoints

    Scopes

    Flow

    ID Token

    UserInfo Response

    Discovery

    Authentication vs Authorization

    Basic Implementation

    Standards

    Next Steps

    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

    Field
    Description

    registration_endpoint

    Dynamic client registration

    revocation_endpoint

    Token revocation

    introspection_endpoint

    Field
    Description

    scopes_supported

    Available scopes

    response_types_supported

    Supported response types

    grant_types_supported

    Discovery documents should be cached:

    Clients should verify:

    1. issuer matches the expected provider

    2. Required endpoints are present

    3. Needed scopes/grant types are supported

    4. Signing algorithms are acceptable

    • JWKS - Public keys for verification

    • UserInfo - User claims endpoint

    • ID Tokens - Token specification

    {
      "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"
      ]
    }

    Discovery Endpoint

    Response

    Fields

    Core Endpoints

    issuer

    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;
    }

    Additional Endpoints

    Supported Features

    Usage

    JavaScript

    Python

    Caching

    Validation

    Next Steps

    Type
    Description

    sub

    String

    Subject identifier

    Claim
    Type
    Description

    name

    String

    Full name

    given_name

    Claim
    Type
    Description

    email

    String

    Email address

    email_verified

    Boolean

    Claim
    Type
    Description

    address

    Object

    Address object

    Address object:

    Claim
    Type
    Description

    phone_number

    String

    Phone number

    phone_number_verified

    Boolean

    Aspect
    ID Token
    UserInfo

    Format

    JWT (signed)

    JSON

    When received

    Token response

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

    UserInfo responses can be cached briefly:

    • ID Tokens - Token claims

    • JWKS - Token verification

    • Discovery - Endpoint discovery

    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
    }

    Endpoint

    Request

    Authorization Header

    POST with Form Body

    Response

    Claims by Scope

    openid (required)

    {
      "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;
      }
    }

    profile

    email

    address

    phone

    Usage

    JavaScript

    Python

    Error Responses

    Invalid Token

    Expired Token

    Insufficient Scope

    UserInfo vs ID Token

    Caching

    Next Steps

    sub

    Subject (unique user ID)

    aud

    Audience (client ID)

    exp

    Expiration time

    iat

    Issued at time

    Claim
    Description

    auth_time

    Time of authentication

    nonce

    Value from authorization request

    at_hash

    Claim
    Description

    name

    Full name

    given_name

    First name

    family_name

    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)

    Nonce prevents replay attacks:

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

    • Check JWKS URL is correct

    • Verify key ID (kid) matches

    • Ensure algorithm is RS256

    • Check server/client time sync

    • Token may have short lifetime

    • Refresh authentication

    • Verify client ID in validation

    • Check token was issued for your client

    • JWKS - Token verification keys

    • UserInfo - Additional claims

    • Discovery - Provider configuration

    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
    }

    iss

    Issuer (Authority URL)

    Overview

    Token Structure

    Decoded Token

    Header

    Payload

    Standard Claims

    Required Claims

    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');
        }
      }
    }

    Optional Claims

    Profile Claims

    Validation

    Required Steps

    JavaScript Example

    Python Example

    Using Nonce

    Request

    Validation

    at_hash Validation

    Common Issues

    "Invalid signature"

    "Token expired"

    "Invalid audience"

    Next Steps

    4000

    HTTP server port

    HOST

    0.0.0.0

    Bind address

    BASE_URL

    http://localhost:4000

    Public URL

    CRYSTAL_WORKERS

    4

    Worker processes

    Variable
    Default
    Description

    DATABASE_URL

    Required

    PostgreSQL connection string

    Variable
    Default
    Description

    SECRET_KEY

    Required

    JWT signing key (256-bit)

    See Security Settings for security-specific options.

    Variable
    Default
    Description

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    CODE_TTL

    600

    See Token Settings for token-specific options.

    Variable
    Default
    Description

    SESSION_KEY

    session_id

    Session cookie name

    SESSION_DURATION_DAYS

    7

    Variable
    Default
    Description

    SSL_CERT

    Path to certificate

    SSL_KEY

    Variable
    Default
    Description

    TEMPLATES_PATH

    ./public/templates

    Template directory

    Variable
    Default
    Description

    CRYSTAL_LOG_LEVEL

    debug

    Log level

    CRYSTAL_LOG_SOURCES

    *

    Variable
    Default
    Description

    SMTP_HOST

    SMTP server host

    SMTP_PORT

    587

    Variable
    Default
    Description

    REDIS_URL

    Redis connection URL

    Variable
    Default
    Description

    RATE_LIMIT_ENABLED

    true

    Enable rate limiting

    RATE_LIMIT_BY

    ip

    Variable
    Default
    Description

    APP_NAME

    Authority

    Application name

    APP_TAGLINE

    Variable
    Default
    Description

    ENABLE_PASSWORD_GRANT

    false

    Enable password grant

    PASSWORD_GRANT_ALLOWED_CLIENTS

    • Security Settings - Security configuration

    • Token Settings - Token configuration

    • Environment Variables - Setup guide

    CRYSTAL_ENV

    development

    Environment: development, production, test

    Server

    PORT

    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

    Database

    Security

    Tokens

    Sessions

    SSL/TLS

    Templates

    Logging

    Email

    Redis

    Rate Limiting

    Branding

    OAuth

    Example Configuration

    Development

    Production

    Next Steps

    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

    Variable
    Default
    Description

    PASSWORD_MIN_LENGTH

    12

    Minimum password length

    PASSWORD_HISTORY_COUNT

    5

    Variable
    Default
    Description

    ADMIN_PASSWORD_MIN_LENGTH

    16

    Admin minimum length

    ADMIN_PASSWORD_EXPIRY_DAYS

    30

    Variable
    Default
    Description

    REQUIRE_MFA

    false

    Require MFA for all users

    REQUIRE_ADMIN_MFA

    true

    Variable
    Default
    Description

    SESSION_DURATION_DAYS

    7

    Maximum session lifetime

    IDLE_TIMEOUT_MINUTES

    30

    Variable
    Default
    Description

    ADMIN_ALLOWED_IPS

    IP whitelist for admin

    HIDE_EMAIL_EXISTENCE

    true

    Variable
    Default
    Description

    PASSWORD_RESET_TTL

    3600

    Reset token lifetime (seconds)

    PASSWORD_RESET_RATE_LIMIT

    3

    Variable
    Default
    Description

    SECRET_KEY

    Required

    JWT signing key

    JWT_ALGORITHM

    RS256

    Variable
    Default
    Description

    AUDIT_LOG_RETENTION_DAYS

    90

    Log retention period

    AUDIT_LOG_SYSLOG

    false

    • Token Settings - Token configuration

    • All Options - Complete reference

    • Enable MFA - MFA setup

    LOCKOUT_THRESHOLD

    5

    Failed attempts before lockout

    Account Lockout

    # 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

    Password Policies

    Admin Password Policies

    Multi-Factor Authentication

    Session Security

    Access Control

    Password Reset

    JWT Security

    Audit Logging

    Example: High Security

    Example: User-Friendly

    Next Steps

    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)

    Variable
    Default
    Description

    REFRESH_TOKEN_ROTATION

    true

    Rotate on refresh

    REFRESH_TOKEN_REUSE_INTERVAL

    0

    Variable
    Default
    Description

    ACCESS_TOKEN_FORMAT

    jwt

    jwt or opaque

    JWT_ALGORITHM

    RS256

    Configure per-client token settings via API:

    Claim
    Description

    iss

    Issuer (Authority URL)

    sub

    Subject (user ID)

    aud

    Claim
    Description

    iss

    Issuer

    sub

    Subject

    aud

    Variable
    Default
    Description

    INTROSPECTION_CACHE_TTL

    60

    Cache introspection results

    Variable
    Default
    Description

    REVOKE_REFRESH_ON_PASSWORD_CHANGE

    true

    Revoke on password change

    REVOKE_ALL_ON_LOGOUT

    false

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

    • Use for API authorization only

    • Validate on each request

    • Enable rotation

    • Set reasonable lifetime

    • Revoke on security events

    • Very short-lived (5-10 minutes)

    • Single use only

    • Bind to client

    • Security Settings - Security configuration

    • All Options - Complete reference

    • Token Lifecycle - Conceptual overview

    ACCESS_TOKEN_TTL

    3600

    Access token lifetime (seconds)

    Token Lifetimes

    REFRESH_TOKEN_TTL

    {
      "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

    Refresh Token Settings

    Token Format

    Client-Specific Overrides

    Token Claims

    Access Token Claims

    ID Token Claims

    Introspection Settings

    Revocation Settings

    Example Configurations

    Short-Lived Tokens (High Security)

    Long-Lived Tokens (User Convenience)

    API-Only (No Refresh)

    Token Lifecycle

    Best Practices

    Access Tokens

    Refresh Tokens

    Authorization Codes

    Next Steps

  • Auth Endpoints - Login, logout, password reset

  • Business logic is encapsulated in services:

    Service
    Responsibility

    AuthenticationService

    User authentication, MFA validation

    TokenService

    Token generation, validation, revocation

    UserService

    Data access and persistence:

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

    • Repositories - Database queries

    • Migrations - Schema management

    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

    Authority supports multiple instances:

    Shared state stored in Redis:

    • User sessions

    • Rate limit counters

    • Token cache (optional)

    Layer
    Technology

    Language

    Crystal

    Web Framework

    Azu

    Database

    • Security Model - Security architecture

    • Token Lifecycle - Token management

    • OAuth 2.0 Concepts - Protocol fundamentals

    System Overview

    Component Architecture

    Endpoints Layer

    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  │
                  └───────────┘ └───────────┘

    Services Layer

    Data Layer

    Request Flow

    Authorization Code Flow

    Token Validation Flow

    Data Model

    Core Entities

    User

    Client

    Security Architecture

    Defense in Depth

    Token Security

    Scalability

    Horizontal Scaling

    Redis for State

    Technology Stack

    Next Steps

    No way to revoke access without changing passwords

    OAuth provides:

    • Delegated authorization (no password sharing)

    • Scoped access (limited permissions)

    • Revocable tokens (easy access removal)

    Role
    Description

    Resource Owner

    The user who owns the data

    Client

    The application requesting access

    Authorization Server

    Token
    Purpose
    Lifetime

    Authorization Code

    Exchanged for tokens

    Minutes

    Access Token

    Authorizes API requests

    Scopes define what access is granted:

    Clients request scopes:

    Users consent to scopes:

    Best for: Web applications with a backend

    The client never sees the user's password.

    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

    4. Client proves identity with code_verifier

    Best for: Machine-to-machine

    No user involved - the service itself is authorized.

    Best for: TVs, CLI tools, IoT

    1. User wants to access protected resource

      User clicks "Login with Authority" in your app.

    2. Client redirects to Authorization Server

      GET /authorize?
        response_type=code
        &client_id=abc123
        &redirect_uri=https://app.example.com/callback
        &scope=profile email
        &state=xyz789
    3. User authenticates

      Authority shows login page. User enters credentials.

    4. User authorizes

      Authority shows consent screen. User approves scopes.

    5. Authorization Server redirects back

    6. Client exchanges code for tokens

    7. Client receives tokens

    8. Client uses access token

    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

    5. Client verifies state matches

    Prevents authorization code interception:

    • Short-lived access tokens - Limit exposure window

    • Token rotation - New refresh token each use

    • Secure storage - Never store in URL or logs

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

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

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

    • OpenID Connect Concepts - Identity layer

    • Choosing Grant Types - Decision guide

    • Token Lifecycle - Token management

    What is OAuth 2.0?

    The Problem OAuth Solves

    ┌──────────────────────────────────────────────────┐
    │                                                  │
    │   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)
                                Attacker
                                   │
    ┌─────────┐                    │                    ┌─────────┐
    │ Client  │───code_challenge───┼───────────────────►│Authority│
    │         │◄──authorization_code◄──────────────────│         │
    │         │───code_verifier────┼────────────────────►│         │
    │         │◄──access_token─────┼────────────────────│         │
    └─────────┘                    │                    └─────────┘
                                   │
                        Attacker has code,
                        but can't prove
                        they know verifier

    Key Concepts

    Roles

    Tokens

    Scopes

    Grant Types

    Authorization Code (Most Common)

    Authorization Code + PKCE

    Client Credentials

    Device Code

    The Authorization Flow

    Step by Step

    Security Concepts

    State Parameter

    PKCE

    Token Security

    Common Misconceptions

    "OAuth is for authentication"

    "The access token contains user data"

    "Longer token lifetimes are more secure"

    Next Steps

    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.

    OIDC adds:

    • ID Token - Proof of authentication

    • UserInfo Endpoint - User profile data

    • Standard Scopes - openid, profile, email

    • Discovery - Automatic configuration

    The ID token is a JWT that proves authentication:

    Claim
    Description

    iss

    Who issued the token

    sub

    Unique user identifier

    aud

    Request identity information with scopes:

    Scope
    Claims

    openid

    sub (required)

    profile

    name, picture, etc.

    email

    Aspect
    ID Token
    Access Token

    For

    Client

    Resource Server

    Purpose

    Prove identity

    Get more user data with the access token:

    Response:

    Aspect
    ID Token
    UserInfo

    When

    Token response

    Separate request

    Format

    JWT (signed)

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

    OIDC providers publish configuration:

    Response:

    Clients use discovery to automatically configure themselves.

    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

    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

    • ID tokens are signed, not encrypted

    • Claims can be read by anyone

    • Don't put sensitive data in ID tokens

    • ID tokens prove identity at login time

    • Don't use ID tokens for API authorization

    • Use access tokens for API calls

    • OAuth 2.0 Concepts - Authorization fundamentals

    • Token Lifecycle - Token management

    • ID Tokens Reference - Technical details

    Purpose

    Authorization

    Authentication

    OAuth vs OpenID Connect

    ┌────────────────────────────────────────┐
    │           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

    The Identity Layer

    The ID Token

    Key Claims

    Standard Scopes

    Authentication Flow

    ID Token vs Access Token

    When to Use Which

    UserInfo Endpoint

    ID Token vs UserInfo

    Discovery

    Nonce

    Authentication Patterns

    Simple Login

    Single Sign-On (SSO)

    Silent Authentication

    Security Considerations

    Validate ID Tokens

    Don't Trust Claims Blindly

    Use ID Token for Authentication Only

    Next Steps

    Validated - Against policy requirements
  • History-checked - Prevent reuse

  • TOTP-based MFA provides:

    • Something you know - Password

    • Something you have - Authenticator app

    MFA protects against:

    • Credential stuffing

    • Phishing

    • Password breaches

    Progressive lockout:

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

    Tokens are signed with RS256:

    • Private key - Signs tokens (server only)

    • Public key - Verifies tokens (anyone)

    • Key rotation - Periodic key changes

    Token
    Lifetime
    Purpose

    Authorization Code

    10 minutes

    Minimize interception window

    Access Token

    1 hour

    Refresh tokens rotate on use:

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

    Tokens can be revoked:

    • User logout

    • Password change

    • Admin action

    • Security concern

    Sessions are bound to:

    • User agent

    • IP address (optional)

    • Expiration time

    • Absolute timeout - Maximum session lifetime

    • Idle timeout - Inactivity limit

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

    • Exact match required

    • No wildcards

    • HTTPS in production

    • Pre-registered only

    Prevents CSRF:

    Protects public clients:

    Data
    Storage
    Access

    Passwords

    Hashed (bcrypt)

    Never retrievable

    MFA secrets

    Encrypted

    • Connection encryption (SSL)

    • Prepared statements (SQL injection prevention)

    • Principle of least privilege

    • Regular backups

    • Authentication attempts (success/failure)

    • Token operations (issue/revoke)

    • Admin actions

    • Configuration changes

    Each log entry includes:

    • Timestamp

    • Actor (user/client/system)

    • Action

    • Resource

    • IP address

    • User agent

    • Changes made

    • Default: 90 days

    • Configurable per compliance requirements

    • Export for external systems

    Threat
    Mitigation

    Credential stuffing

    Lockout, MFA, rate limiting

    Token theft

    Short lifetimes, rotation, binding

    CSRF

    1. Always use HTTPS - No exceptions

    2. Use strong secrets - Cryptographically random

    3. Enable MFA - At least for admins

    4. Monitor logs - Regular review

    5. Keep updated - Apply security patches

    1. Validate tokens - Always verify signatures

    2. Use PKCE - For all public clients

    3. Store securely - No localStorage for sensitive tokens

    4. Handle errors - Don't leak information

    5. Implement logout - Revoke tokens properly

    • Token Lifecycle - Token management details

    • Architecture - System design

    • Enable MFA - MFA setup guide

    ┌─────────────────────────────────────────┐
    │        Network Security (HTTPS)          │
    ├─────────────────────────────────────────┤
    │           Rate Limiting                  │
    ├─────────────────────────────────────────┤
    │         Input Validation                 │
    ├─────────────────────────────────────────┤
    │   Authentication (Password + MFA)        │
    ├─────────────────────────────────────────┤
    │       Session Management                 │
    ├─────────────────────────────────────────┤
    │     Authorization (Scopes)               │
    ├─────────────────────────────────────────┤
    │         Audit Logging                    │
    └─────────────────────────────────────────┘

    Security Architecture

    Defense in Depth

    Authentication Security

    Password Security

    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

    Multi-Factor Authentication

    Account Lockout

    Token Security

    JWT Signing

    Token Lifetimes

    Token Rotation

    Token Revocation

    Session Security

    Session Binding

    Session Limits

    OAuth Security

    Redirect URI Validation

    State Parameter

    PKCE

    Data Protection

    Sensitive Data

    Database Security

    Audit and Compliance

    Logged Events

    Log Contents

    Retention

    Threat Mitigation

    Best Practices

    Deployment

    Client Implementation

    Next Steps

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

  • Authorizes API requests. Presented to resource servers.

    Authority issues JWTs:

    Resource servers validate by:

    1. Verifying JWT signature (JWKS)

    2. Checking expiration

    3. Verifying issuer and audience

    4. Checking required scopes

    Obtains new access tokens without user interaction.

    With rotation enabled:

    Optional reuse window for concurrent requests:

    Proves user authentication. Contains identity claims.

    Must validate:

    1. Signature (JWKS)

    2. Issuer matches expected

    3. Audience contains client ID

    4. Expiration not passed

    5. Nonce matches (if sent)

    Tokens are revoked when:

    • User logs out

    • User changes password

    • Admin revokes manually

    • Security incident detected

    • Session ends

    For real-time validation:

    Token
    Storage
    Notes

    Access

    Memory

    Clear on logout

    Refresh

    Secure cookie

    Environment
    Access Token
    Refresh Token

    High Security

    15 minutes

    1 day

    Standard

    1 hour

    Proactive:

    Reactive:

    • Security Model - Security architecture

    • Refresh Tokens Reference - Technical details

    • Token Settings - Configuration

    Creation:  User approves consent
    Lifetime:  10 minutes (configurable)
    Exchange:  POST /token (one-time use)
    Deletion:  After exchange or expiration

    Token Types Overview

    Token States

    Authorization Code

    Purpose

    Lifecycle

    Security Properties

    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}` }
      });
    }

    Access Token

    Purpose

    Lifecycle

    Format

    Validation

    Refresh Token

    Purpose

    Lifecycle

    Rotation

    Grace Period

    ID Token

    Purpose

    Lifecycle

    Validation

    Token Revocation

    Triggers

    Cascade

    Revocation Check

    Token Storage

    Server-Side

    Client-Side

    Best Practices

    Token Lifetimes

    Refresh Strategy

    Handling Expiration

    Next Steps

    Single-page application

    Authorization Code + PKCE

    High

    Mobile application

    Authorization Code + PKCE

    High

    Server-to-server

    Client Credentials

    High

    CLI tool

    Device Code

    Medium

    Smart TV / IoT

    Device Code

    Medium

    First-party mobile app

    Password (with caution)

    Medium

    • Web applications with server-side code

    • You can securely store a client secret

    • Users need to authenticate

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

    • Traditional web applications (Rails, Django, Express)

    • Server-rendered applications

    • Applications with session-based auth

    • Requires backend infrastructure

    • Client secret must be protected

    • Full OAuth security guarantees

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

    • Mobile applications (iOS, Android)

    • Desktop applications

    • Any public client (cannot keep secrets)

    • React/Vue/Angular SPAs

    • React Native / Flutter apps

    • Electron desktop apps

    • Browser extensions

    • No client secret required

    • Proof of possession via PKCE

    • Recommended for all new public clients

    • Backend services calling APIs

    • Microservice communication

    • Scheduled jobs / cron tasks

    • No user context needed

    • Payment processing service

    • Data synchronization jobs

    • Inter-service authentication

    • API integrations

    • No user involved

    • No refresh tokens (just request new token)

    • Client must protect its credentials

    • Scopes limited to service-level access

    • Devices without browsers

    • Limited input capability

    • Smart TVs, game consoles

    • CLI applications

    • Smart TV streaming apps

    • Game console apps

    • CLI tools (gh, aws-cli style)

    • IoT devices

    • Printers / picture frames

    • User must have secondary device

    • Requires polling mechanism

    • Codes expire (typically 5-15 minutes)

    • Good UX requires clear instructions

    • Migrating from legacy systems

    • Highly trusted first-party apps

    • When redirect flow is impossible

    • User enters password in your app

    • App sees user's credentials

    • No consent flow

    • MFA may be bypassed

    • Never for third-party apps

    Grant
    Secret Protection
    Token Exposure
    Phishing Risk

    Auth Code

    High

    Low

    Low

    Grant
    User Steps
    Complexity
    Best For

    Auth Code

    Login + Consent

    Medium

    Web apps

    1. Add PKCE support to your app

    2. Implement authorization flow

    3. Migrate users gradually

    4. Disable password grant

    1. Add PKCE code exchange

    2. Update authorization request (response_type=code)

    3. Handle code callback

    4. Remove fragment token handling

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

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

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

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

    • OAuth 2.0 Concepts - Protocol fundamentals

    • Security Model - Security considerations

    • Authorization Code Reference - Implementation details

    Web app with backend

    Authorization Code

    High

    Decision Flowchart

    Quick Reference

    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

    Authorization Code

    When to Use

    How It Works

    Example Scenarios

    Considerations

    Authorization Code + PKCE

    When to Use

    How It Works

    Example Scenarios

    Considerations

    Client Credentials

    When to Use

    How It Works

    Example Scenarios

    Considerations

    Device Code

    When to Use

    How It Works

    Example Scenarios

    Considerations

    Password Grant (Legacy)

    When to Use

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

    How It Works

    Considerations

    Grant Type Comparison

    Security

    User Experience

    Migration Paths

    From Password to PKCE

    From Implicit to PKCE

    Common Mistakes

    Using Implicit Grant

    Password Grant for Third-Party Apps

    No PKCE for Public Clients

    Long-Lived Tokens Without Refresh

    Next Steps

    Modern admin dashboard

  • Self-hosted deployment

  • 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

    No per-user or per-authentication pricing:

    • Fixed infrastructure costs

    • Scale without cost surprises

    • No vendor lock-in

    • Budget predictability

    Complete control over the experience:

    • Custom login pages

    • Branded emails

    • Custom flows

    • Extended functionality

    Your security, your way:

    • Configure security policies

    • Custom audit requirements

    • Integration with existing security tools

    • Incident response control

    All standard grant types:

    Grant Type
    Use Case

    Authorization Code

    Web applications

    Authorization Code + PKCE

    Mobile / SPA

    Client Credentials

    Full identity support:

    • ID tokens for authentication

    • UserInfo endpoint

    • Discovery document

    • JWKS for verification

    Production-ready security:

    • Multi-factor authentication (TOTP)

    • Account lockout

    • Password policies

    • Session management

    • Comprehensive audit logging

    • Rate limiting

    Modern management interface:

    • OAuth client management

    • User administration

    • Scope configuration

    • Audit log viewer

    • System settings

    Build authentication for your SaaS product:

    • Single sign-on across services

    • User management

    • Third-party integrations

    Secure internal applications:

    • Employee authentication

    • Service-to-service auth

    • API gateway integration

    • Audit compliance

    Power developer ecosystems:

    • OAuth for third-party apps

    • API access control

    • Developer portal integration

    • Rate limiting per client

    Authenticate devices and CLIs:

    • Device code flow

    • Machine credentials

    • Token management

    Aspect
    Authority
    Auth0/Okta

    Hosting

    Self-hosted

    Cloud

    Pricing

    Infrastructure only

    Aspect
    Authority
    Keycloak

    Language

    Crystal

    Java

    Memory footprint

    Low

    Aspect
    Authority
    Custom

    Time to production

    Hours

    Months

    Security review

    Done

    1. Configure environment

    2. Set up database

    3. Enable HTTPS

    4. Create admin user

    5. Register clients

    See Installation Guide.

    Authority implements:

    • RFC 6749 - OAuth 2.0

    • RFC 6750 - Bearer Tokens

    • RFC 7519 - JWT

    • RFC 7636 - PKCE

    • - Introspection

    • - Revocation

    • - Device Authorization

    Built with modern, efficient technology:

    Component
    Technology

    Language

    Crystal

    Framework

    Azu

    Database

    Crystal provides:

    • Performance - Near C speed

    • Safety - Type-safe, null-safe

    • Simplicity - Ruby-like syntax

    • Efficiency - Low memory footprint

    • Quick Start Tutorial - Get running in 5 minutes

    • Architecture - System design

    • Security Model - Security features

    What is Authority?

    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

    Why Self-Host Authentication?

    Data Sovereignty

    Cost Predictability

    Customization

    Security Control

    Key Features

    Complete OAuth 2.0

    OpenID Connect

    Enterprise Security

    Admin Dashboard

    Use Cases

    SaaS Applications

    Internal Tools

    Developer Platforms

    IoT / Device Authentication

    Comparison

    vs. Auth0 / Okta

    vs. Keycloak

    vs. Build Your Own

    Getting Started

    Quick Start

    Production Deployment

    Standards Compliance

    Technology

    Next Steps

    Email, username, avatar

    Facebook

    email, public_profile

    Email, name, profile picture

    LinkedIn

    openid, profile, email

    Professional profile data

    Apple

    name, email

    Privacy-focused, email relay option

    User's username or email

    password

    Yes

    User's password

    scope

    Optional

    Requested scopes

    Token introspection

    device_authorization_endpoint

    Device flow

    Supported grant types

    token_endpoint_auth_methods_supported

    Client authentication methods

    claims_supported

    Available user claims

    String

    First name

    family_name

    String

    Last name

    middle_name

    String

    Middle name

    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

    Email verified

    Phone verified

    Separate request

    Purpose

    Authentication proof

    Additional claims

    Freshness

    Issued at auth time

    Current values

    Access token hash

    c_hash

    Code hash (for hybrid flow)

    acr

    Authentication context class

    amr

    Authentication methods used

    azp

    Authorized party

    Last name

    email

    Email address

    email_verified

    Email verified

    picture

    Profile picture URL

    Authorization code lifetime (seconds)

    DEVICE_CODE_TTL

    300

    Device code lifetime (seconds)

    Session lifetime (days)

    IDLE_TIMEOUT_MINUTES

    30

    Idle timeout

    SINGLE_SESSION

    false

    Allow only one session

    Path to private key

    SSL_CA

    Path to CA certificate

    SSL_MODE

    SSL mode

    Log sources

    SMTP server port

    SMTP_USER

    SMTP username

    SMTP_PASSWORD

    SMTP password

    SMTP_FROM

    Sender email

    SMTP_FROM_NAME

    Authority

    Sender name

    Rate limit key

    RATE_LIMIT_WINDOW

    60

    Window (seconds)

    RATE_LIMIT_MAX

    60

    Max requests

    RATE_LIMIT_WHITELIST

    Exempt IPs

    Application tagline

    COMPANY_NAME

    Company name

    THEME

    dark

    Theme (dark/light)

    Allowed client IDs

    DEFAULT_SCOPES

    openid

    Default scopes

    Prevent password reuse

    PASSWORD_EXPIRY_DAYS

    0

    Password expiration (0=never)

    PASSWORD_EXPIRY_WARNING_DAYS

    14

    Warn before expiry

    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

    Admin expiration

    Require MFA for admins

    MFA_GRACE_PERIOD_DAYS

    7

    Time to set up MFA

    Idle timeout

    SINGLE_SESSION

    false

    Only one active session

    NOTIFY_NEW_SESSION

    false

    Email on new login

    REVOKE_TOKENS_ON_SESSION_END

    true

    Revoke tokens on logout

    Don't reveal if email exists

    Max resets per window

    PASSWORD_RESET_RATE_WINDOW

    3600

    Rate limit window (seconds)

    Signing algorithm

    Send to syslog

    SYSLOG_HOST

    Syslog server

    SYSLOG_PORT

    514

    Syslog port

    Server-to-server

    Device Code

    IoT / CLI

    Refresh Token

    Token renewal

    Per user

    Data location

    Your servers

    Their servers

    Customization

    Full

    Limited

    Vendor lock-in

    None

    High

    High

    Complexity

    Simple

    Complex

    Features

    Core OAuth/OIDC

    Enterprise IAM

    Learning curve

    Gentle

    Steep

    Required

    Maintenance

    Updates

    All on you

    Standards compliance

    Complete

    Variable

    PostgreSQL

    Caching

    Redis

    Templates

    Crinja

    RFC 7662
    RFC 7009
    RFC 8628
    OpenID Connect Core 1.0
    spinner

    Step 3: Handle the Callback

    Step 4: Exchange Code for Tokens

    Step 5: Use the Access Token

    Complete Example

    Next Steps

    spinner
    spinner
    spinner
    spinner
    spinner

    Issues tokens after authorization

    Resource Server

    Hosts the protected resources (API)

    Hours

    Refresh Token

    Gets new access tokens

    Days/Weeks

    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...

    User claims endpoint

    /authorize

    Authorization (with OIDC scopes)

    /token

    Token (returns ID token)

    email, email_verified

    address

    address

    phone

    phone_number, phone_number_verified

    ID token

    Question answered

    "What can they do?"

    "Who are they?"

    Scope

    Custom scopes

    openid, profile, etc.

    Grace period for reuse (seconds)

    REFRESH_TOKEN_ABSOLUTE_TTL

    31536000

    Absolute lifetime (1 year)

    Signing algorithm

    Audience (client ID)

    exp

    Expiration time

    iat

    Issued at time

    scope

    Granted scopes

    client_id

    Client identifier

    Audience

    exp

    Expiration

    iat

    Issued at

    auth_time

    Authentication time

    nonce

    Request nonce

    at_hash

    Access token hash

    Revoke all tokens on logout

    User CRUD, password management

    ClientService

    OAuth client management

    SessionService

    Session creation, validation, cleanup

    AuditService

    Event logging, compliance

    PostgreSQL

    Cache

    Redis

    Templates

    Crinja (Jinja2)

    JWT

    crystal-jwt

    Who the token is for

    exp

    When it expires

    iat

    When it was issued

    auth_time

    When user authenticated

    nonce

    Prevents replay attacks

    email, email_verified

    address

    address

    phone

    phone_number

    Authorize API calls

    Validate

    At login

    On each request

    Contains

    User identity

    Authorization grants

    JSON

    Freshness

    Point-in-time

    Current values

    Limit exposure if stolen

    Refresh Token

    30 days

    User convenience

    ID Token

    1 hour

    Authentication proof

    User + admin

    Client secrets

    Hashed

    Never retrievable

    Tokens

    Database + cache

    Service only

    State parameter, SameSite cookies

    Code interception

    PKCE, short code lifetime

    Replay attacks

    Nonce, one-time codes

    Brute force

    Lockout, progressive delay

    httpOnly, secure

    ID

    Memory

    User info display

    30 days

    User-Friendly

    1 day

    90 days

    Auth Code + PKCE

    Medium

    Low

    Low

    Client Credentials

    High

    N/A

    N/A

    Device Code

    Medium

    Low

    Medium

    Password

    Low

    Medium

    High

    Auth Code + PKCE

    Login + Consent

    Medium

    Mobile/SPA

    Client Credentials

    None

    Low

    Services

    Device Code

    Visit URL + Enter code

    High

    Limited input

    Password

    Enter credentials

    Low

    Legacy

    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    User Interface Reference
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner
    spinner