Files
gatehouse-api/docs/OIDC.md
T

43 KiB

OpenID Connect (OIDC) Provider Documentation

This document provides comprehensive documentation for the Authy2 OIDC (OpenID Connect) provider implementation. Use this as the main reference for integrating with the OIDC provider.

Table of Contents

  1. Overview
  2. Quick Start
  3. API Endpoints Reference
  4. OIDC Client Configuration
  5. Integration Examples
  6. Security Considerations
  7. Deployment Checklist
  8. Troubleshooting

Overview

What is OIDC?

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that allows clients to verify the identity of end-users and obtain basic profile information. It enables single sign-on (SSO) capabilities across applications.

Why Use OIDC?

  • Standardized Authentication: Industry-standard protocol with broad client library support
  • User Identity Verification: Verifies user identity through ID tokens (JWTs)
  • Scoped Access: Request specific user information with granular permissions
  • Security: Built-in support for PKCE, token rotation, and secure token handling
  • Interoperability: Works with numerous identity providers and client applications

Integration with Authy2

The OIDC provider integrates with the existing Authy2 authentication system:

┌─────────────────────────────────────────────────────────────┐
│                     OIDC Provider                            │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ Discovery   │  │ Authorization│  │ Token Endpoint      │  │
│  │ Endpoint    │  │  Endpoint    │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ UserInfo    │  │ JWKS        │  │ Revocation/Introspect│ │
│  │ Endpoint    │  │ Endpoint    │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│              Authy2 Core Services                            │
├─────────────────────────────────────────────────────────────┤
│  • User Service       • Session Service    • Audit Service  │
│  • Auth Service       • OIDC Token Service                   │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    PostgreSQL Database                        │
├─────────────────────────────────────────────────────────────┤
│  • Users    • OIDC Clients    • OIDC Authorization Codes    │
│  • Sessions • Refresh Tokens  • Token Metadata & Audit Logs │
└─────────────────────────────────────────────────────────────┘

Supported OIDC Flows

Flow Support Description
Authorization Code with PKCE Full Recommended for all clients
Authorization Code ⚠️ Deprecated PKCE required for new clients
Refresh Token Full Token rotation supported

Quick Start

Prerequisites

  1. Python 3.9+ with pip
  2. PostgreSQL 13+ database
  3. Redis (optional, for session storage)
  4. OIDC Client Library for your platform

Installation

  1. Clone the repository and install dependencies:
git clone <repository-url>
cd backend
pip install -r requirements/base.txt
  1. Set up environment variables:
cp .env.example .env
# Edit .env with your configuration
  1. Run database migrations:
python manage.py db upgrade

Database Setup

The OIDC provider requires the following tables (automatically created via migrations):

  • oidc_clients - Registered OIDC clients
  • oidc_authorization_codes - Temporary authorization codes
  • oidc_refresh_tokens - Refresh tokens with rotation support
  • oidc_sessions - OIDC session tracking
  • oidc_token_metadata - Token metadata for revocation
  • oidc_audit_logs - Audit trail for all OIDC operations

Basic Configuration

Configure the following environment variables:

# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/authy2

# Redis (optional)
REDIS_URL=redis://localhost:6379/0

# OIDC
OIDC_ISSUER_URL=http://localhost:5000

# Security
SECRET_KEY=your-secure-secret-key-min-32-chars
BCRYPT_LOG_ROUNDS=12

# Logging
LOG_LEVEL=INFO

Creating Your First OIDC Client

Register a new OIDC client using the registration endpoint:

curl -X POST http://localhost:5000/oidc/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Application",
    "redirect_uris": ["http://localhost:8080/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "openid profile email",
    "token_endpoint_auth_method": "client_secret_basic"
  }'

Response:

{
  "version": "1.0",
  "success": true,
  "code": 201,
  "message": "Client registered successfully",
  "request_id": "...",
  "data": {
    "client_id": "oidc_abc123...",
    "client_secret": "secret_xyz789...",
    "client_id_issued_at": 1704067200,
    "client_secret_expires_at": 0,
    "client_name": "My Application",
    "redirect_uris": ["http://localhost:8080/callback"],
    "token_endpoint_auth_method": "client_secret_basic",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "openid profile email"
  }
}

Important: Save the client_id and client_secret securely. The client_secret will not be shown again.


API Endpoints Reference

All endpoints follow the standard API response format documented in docs/architecture.md.

1. Discovery Endpoint

URL: GET /.well-known/openid-configuration

Returns the OIDC provider configuration as JSON.

Request:

curl http://localhost:5000/.well-known/openid-configuration

Response:

{
  "issuer": "http://localhost:5000",
  "authorization_endpoint": "http://localhost:5000/oidc/authorize",
  "token_endpoint": "http://localhost:5000/oidc/token",
  "userinfo_endpoint": "http://localhost:5000/oidc/userinfo",
  "jwks_uri": "http://localhost:5000/oidc/jwks",
  "registration_endpoint": "http://localhost:5000/oidc/register",
  "revocation_endpoint": "http://localhost:5000/oidc/revoke",
  "introspection_endpoint": "http://localhost:5000/oidc/introspect",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "response_modes_supported": ["query"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "claims_supported": ["sub", "name", "email", "email_verified"]
}

Headers:

  • Cache-Control: max-age=86400 (cached for 24 hours)

Status Codes:

  • 200 - Success
  • 500 - Server error

2. Authorization Endpoint

URL: GET/POST /oidc/authorize

Initiates the OIDC authentication flow. Supports both GET (browser redirect) and POST (direct API) requests.

Request Parameters (GET/POST):

Parameter Type Required Description
client_id string Yes The client ID
redirect_uri string Yes Redirect URI after authorization
response_type string Yes Must be "code"
scope string Yes Space-separated scopes (e.g., "openid profile email")
state string Recommended Opaque state for CSRF protection
nonce string Recommended Nonce for ID token replay protection
code_challenge string For PKCE PKCE code challenge
code_challenge_method string For PKCE "S256" or "plain"
prompt string No "login", "consent", "select_account", "none"
max_age integer No Maximum authentication age in seconds
acr_values string No Requested Authentication Context Class Reference

POST-only Parameters:

Parameter Type Required Description
email string Yes* User email (for direct authentication)
password string Yes* User password (for direct authentication)

*Required for POST requests without session

Request (GET - Browser):

http://localhost:5000/oidc/authorize?\
  client_id=YOUR_CLIENT_ID&\
  redirect_uri=http://localhost:8080/callback&\
  response_type=code&\
  scope=openid%20profile%20email&\
  state=YOUR_STATE&\
  nonce=YOUR_NONCE&\
  code_challenge=YOUR_CODE_CHALLENGE&\
  code_challenge_method=S256

Request (POST - Direct API):

curl -X POST http://localhost:5000/oidc/authorize \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "redirect_uri=http://localhost:8080/callback" \
  -d "response_type=code" \
  -d "scope=openid profile email" \
  -d "state=YOUR_STATE" \
  -d "nonce=YOUR_NONCE" \
  -d "code_challenge=YOUR_CODE_CHALLENGE" \
  -d "code_challenge_method=S256" \
  -d "email=user@example.com" \
  -d "password=UserPassword123!"

Success Response (302 Redirect):

HTTP/1.1 302 Found
Location: http://localhost:8080/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE

Error Response (302 Redirect with Error):

HTTP/1.1 302 Found
Location: http://localhost:8080/callback?error=invalid_request&error_description=Invalid+client_id&state=YOUR_STATE

Error Codes:

Error Code Description
invalid_request Missing or invalid required parameter
unauthorized_client Client not authorized for this flow
unsupported_response_type response_type not supported
invalid_scope Invalid or disallowed scope
invalid_request Invalid redirect_uri

Status Codes:

  • 302 - Redirect to callback URL
  • 200 - Login page (GET when not authenticated)
  • 400 - Invalid request

3. Token Endpoint

URL: POST /oidc/token

Exchanges authorization codes for tokens or refreshes tokens.

Request Headers:

  • Content-Type: application/x-www-form-urlencoded

Request Body:

Parameter Type Required Description
grant_type string Yes "authorization_code" or "refresh_token"
client_id string Yes* The client ID
client_secret string Yes* The client secret

*Required if not using Basic authentication

For authorization_code grant:

Parameter Type Required Description
code string Yes The authorization code
redirect_uri string Yes The redirect URI used in authorization
code_verifier string For PKCE PKCE code verifier

For refresh_token grant:

Parameter Type Required Description
refresh_token string Yes The refresh token
scope string No Optional scope override

Request (Authorization Code):

curl -X POST http://localhost:5000/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=AUTHORIZATION_CODE" \
  -d "redirect_uri=http://localhost:8080/callback" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "code_verifier=YOUR_CODE_VERIFIER"

Request (Refresh Token):

curl -X POST http://localhost:5000/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=YOUR_REFRESH_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Success Response:

{
  "version": "1.0",
  "success": true,
  "code": 200,
  "message": "Tokens issued successfully",
  "request_id": "...",
  "data": {
    "access_token": "eyJ...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "id_token": "eyJ...",
    "refresh_token": "..."
  }
}

Token Response Fields:

Field Type Description
access_token string JWT access token
token_type string Always "Bearer"
expires_in integer Token lifetime in seconds
id_token string JWT ID token
refresh_token string Opaque refresh token (if granted)

Error Response:

{
  "version": "1.0",
  "success": false,
  "code": 400,
  "message": "Invalid authorization code",
  "error": {
    "type": "INVALID_GRANT",
    "details": {
      "error": "invalid_grant",
      "error_description": "Invalid or expired authorization code"
    }
  }
}

Status Codes:

  • 200 - Tokens issued successfully
  • 400 - Invalid request or grant
  • 401 - Invalid client credentials
  • 500 - Server error

4. UserInfo Endpoint

URL: GET/POST /oidc/userinfo

Returns claims about the authenticated user.

Request Headers:

  • Authorization: Bearer {access_token}

Request:

curl -X GET http://localhost:5000/oidc/userinfo \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Response:

{
  "version": "1.0",
  "success": true,
  "code": 200,
  "message": "User info retrieved successfully",
  "request_id": "...",
  "data": {
    "sub": "user-uuid",
    "name": "John Doe",
    "email": "john@example.com",
    "email_verified": true
  }
}

Claims by Scope:

Scope Claims
openid sub
profile name, preferred_username, picture
email email, email_verified

Status Codes:

  • 200 - User info returned
  • 401 - Invalid or expired token
  • 500 - Server error

5. JWKS Endpoint

URL: GET /oidc/jwks

Returns the JSON Web Key Set containing public keys for token verification.

Request:

curl http://localhost:5000/oidc/jwks

Response:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "key-id-123",
      "use": "sig",
      "alg": "RS256",
      "n": "base64-encoded-modulus",
      "e": "AQAB"
    }
  ]
}

Key Properties:

Property Description
kty Key type (always "RSA")
kid Key ID for key selection
use Key usage ("sig" for signature)
alg Algorithm ("RS256")
n RSA modulus (base64url encoded)
e RSA exponent (base64url encoded)

Headers:

  • Cache-Control: max-age=3600 (cached for 1 hour)

Status Codes:

  • 200 - JWKS returned
  • 500 - Server error

6. Token Revocation Endpoint

URL: POST /oidc/revoke

Revokes an access token or refresh token.

Request Headers:

  • Content-Type: application/x-www-form-urlencoded

Request Body:

Parameter Type Required Description
token string Yes The token to revoke
token_type_hint string No "access_token" or "refresh_token"
client_id string Yes* The client ID
client_secret string Yes* The client secret

*Required if not using Basic authentication

Request:

curl -X POST http://localhost:5000/oidc/revoke \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=YOUR_TOKEN" \
  -d "token_type_hint=access_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response:

{
  "version": "1.0",
  "success": true,
  "code": 200,
  "message": "Token revoked successfully",
  "request_id": "..."
}

Notes:

  • Revocation always returns 200, even if token is invalid
  • Both access tokens and refresh tokens can be revoked
  • Revoking a refresh token also invalidates associated access tokens

Status Codes:

  • 200 - Token revoked (or no-op)
  • 400 - Invalid request
  • 401 - Invalid client credentials
  • 500 - Server error

7. Token Introspection Endpoint

URL: POST /oidc/introspect

Returns information about a token's status and claims.

Request Headers:

  • Content-Type: application/x-www-form-urlencoded

Request Body:

Parameter Type Required Description
token string Yes The token to introspect
token_type_hint string No "access_token" or "refresh_token"
client_id string Yes* The client ID
client_secret string Yes* The client secret

*Required if not using Basic authentication

Request:

curl -X POST http://localhost:5000/oidc/introspect \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=YOUR_ACCESS_TOKEN" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Response (Active Token):

{
  "version": "1.0",
  "success": true,
  "code": 200,
  "message": "Token introspection successful",
  "request_id": "...",
  "data": {
    "active": true,
    "iss": "http://localhost:5000",
    "sub": "user-uuid",
    "aud": "YOUR_CLIENT_ID",
    "exp": 1704070800,
    "iat": 1704067200,
    "nbf": 1704067200,
    "jti": "token-jti",
    "client_id": "YOUR_CLIENT_ID",
    "scope": "openid profile email",
    "token_type": "Bearer"
  }
}

Response (Inactive/Expired Token):

{
  "version": "1.0",
  "success": true,
  "code": 200,
  "message": "Token introspection successful",
  "request_id": "...",
  "data": {
    "active": false
  }
}

Status Codes:

  • 200 - Introspection complete
  • 400 - Invalid request
  • 401 - Invalid client credentials
  • 500 - Server error

8. Client Registration Endpoint

URL: POST /oidc/register

Registers a new OIDC client dynamically.

Request Headers:

  • Content-Type: application/json

Request Body:

Parameter Type Required Description
client_name string Yes Display name for the client
redirect_uris array Yes Array of redirect URIs
grant_types array No Array of grant types (default: ["authorization_code", "refresh_token"])
response_types array No Array of response types (default: ["code"])
scope string No Space-separated scopes (default: "openid profile email")
token_endpoint_auth_method string No "client_secret_basic" or "client_secret_post"
logo_uri string No Client logo URL
client_uri string No Client homepage URL
policy_uri string No Privacy policy URL
tos_uri string No Terms of service URL
organization_id string No Organization ID for client ownership

Request:

curl -X POST http://localhost:5000/oidc/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "My Application",
    "redirect_uris": ["http://localhost:8080/callback", "https://myapp.com/callback"],
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "openid profile email",
    "token_endpoint_auth_method": "client_secret_basic",
    "logo_uri": "https://myapp.com/logo.png",
    "client_uri": "https://myapp.com",
    "policy_uri": "https://myapp.com/privacy",
    "tos_uri": "https://myapp.com/terms"
  }'

Success Response (201 Created):

{
  "version": "1.0",
  "success": true,
  "code": 201,
  "message": "Client registered successfully",
  "request_id": "...",
  "data": {
    "client_id": "oidc_abc123...",
    "client_secret": "secret_xyz789...",
    "client_id_issued_at": 1704067200,
    "client_secret_expires_at": 0,
    "client_name": "My Application",
    "redirect_uris": ["http://localhost:8080/callback", "https://myapp.com/callback"],
    "token_endpoint_auth_method": "client_secret_basic",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "openid profile email"
  }
}

Validation Rules:

  • redirect_uris must contain valid URIs with scheme and netloc
  • grant_types must be a subset of ["authorization_code", "refresh_token"]
  • response_types must be a subset of ["code"]
  • scope must be a subset of ["openid", "profile", "email"]

Status Codes:

  • 201 - Client registered successfully
  • 400 - Invalid request or validation error
  • 500 - Server error

OIDC Client Configuration

Client Registration Parameters

Required Fields

Field Type Description
client_name string Human-readable client name
redirect_uris array Array of valid redirect URIs

Optional Fields

Field Type Default Description
grant_types array ["authorization_code", "refresh_token"] Supported grant types
response_types array ["code"] Supported response types
scope string "openid profile email" Space-separated scopes
token_endpoint_auth_method string "client_secret_basic" Client authentication method
logo_uri string - Client logo URL
client_uri string - Client homepage URL
policy_uri string - Privacy policy URL
tos_uri string - Terms of service URL

Redirect URI Validation

The OIDC provider validates redirect URIs according to RFC 6749:

  1. Exact Matching: Redirect URIs are matched exactly (no wildcards)
  2. Scheme Required: Must have http://, https://, or custom scheme
  3. No Fragments: Fragment components (#) are not allowed
  4. Query Parameters: Allowed but must match exactly

Valid Redirect URIs:

https://myapp.com/callback
http://localhost:8080/callback
myapp://oauth/callback

Invalid Redirect URIs:

# Fragment not allowed
https://myapp.com/callback#fragment

# Wildcard not allowed
https://*.myapp.com/callback

# Missing netloc
myapp:callback

Client Authentication Methods

Method Description Use Case
client_secret_basic Basic auth with client_id:client_secret Server-side applications
client_secret_post Credentials in request body Server-side applications
none No authentication (public clients) Mobile/SPA applications

Example: Basic Authentication

# With client credentials in body
curl -X POST http://localhost:5000/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=CODE" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

# With Basic authentication header
curl -X POST http://localhost:5000/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -H "Authorization: Basic $(echo -n 'YOUR_CLIENT_ID:YOUR_CLIENT_SECRET' | base64)" \
  -d "grant_type=authorization_code" \
  -d "code=CODE"

Integration Examples

OAuth2-Proxy Integration

See docs/oauth2-proxy-config.yaml for complete configuration.

Quick Setup:

  1. Register an OIDC client:
curl -X POST http://localhost:5000/oidc/register \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "oauth2-proxy",
    "redirect_uris": ["http://localhost:4180/oauth2/callback"],
    "scope": "openid profile email"
  }'
  1. Create oauth2-proxy.yaml:
provider: "oidc"
oidc_issuer_url: "http://localhost:5000"
client_id: "your-client-id"
client_secret: "your-client-secret"
cookie_secret: "your-random-cookie-secret-min-32-chars"
cookie_name: "_oauth2_proxy"
http_address: "0.0.0.0:4180"
upstream: "http://127.0.0.1:8080/"
redirect_url: "http://localhost:4180/oauth2/callback"
scope: "openid profile email"
  1. Start oauth2-proxy:
oauth2-proxy -config oauth2-proxy.yaml

Generic OIDC Client Integration

Python Example

import requests
import base64
import secrets
import hashlib

class OIDCClient:
    def __init__(self, issuer_url, client_id, client_secret):
        self.issuer_url = issuer_url.rstrip('/')
        self.client_id = client_id
        self.client_secret = client_secret
        
        # Fetch discovery document
        disc_url = f"{self.issuer_url}/.well-known/openid-configuration"
        self.discovery = requests.get(disc_url).json()
    
    def generate_pkce(self):
        """Generate PKCE code verifier and challenge."""
        code_verifier = secrets.token_urlsafe(43)
        code_challenge = hashlib.sha256(code_verifier.encode()).digest()
        code_challenge = base64.urlsafe_b64encode(code_challenge).decode().rstrip('=')
        return code_verifier, code_challenge
    
    def authorize_url(self, redirect_uri, scopes, state=None, nonce=None):
        """Generate authorization URL."""
        params = {
            'client_id': self.client_id,
            'redirect_uri': redirect_uri,
            'response_type': 'code',
            'scope': ' '.join(scopes),
            'state': state or secrets.token_hex(16),
            'nonce': nonce or secrets.token_hex(16),
        }
        
        code_verifier, code_challenge = self.generate_pkce()
        params['code_challenge'] = code_challenge
        params['code_challenge_method'] = 'S256'
        
        # Build URL
        query = '&'.join(f"{k}={requests.utils.quote(v)}" for k, v in params.items())
        return f"{self.discovery['authorization_endpoint']}?{query}", code_verifier
    
    def token(self, code, redirect_uri, code_verifier=None):
        """Exchange authorization code for tokens."""
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': redirect_uri,
            'client_id': self.client_id,
            'client_secret': self.client_secret,
        }
        if code_verifier:
            data['code_verifier'] = code_verifier
        
        response = requests.post(
            self.discovery['token_endpoint'],
            data=data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )
        return response.json()
    
    def userinfo(self, access_token):
        """Get user info."""
        response = requests.get(
            self.discovery['userinfo_endpoint'],
            headers={'Authorization': f'Bearer {access_token}'}
        )
        return response.json()
    
    def refresh(self, refresh_token):
        """Refresh access token."""
        data = {
            'grant_type': 'refresh_token',
            'refresh_token': refresh_token,
            'client_id': self.client_id,
            'client_secret': self.client_secret,
        }
        response = requests.post(
            self.discovery['token_endpoint'],
            data=data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )
        return response.json()

# Usage
client = OIDCClient(
    issuer_url="http://localhost:5000",
    client_id="your-client-id",
    client_secret="your-client-secret"
)

# Get authorization URL
auth_url, code_verifier = client.authorize_url(
    redirect_uri="http://localhost:8080/callback",
    scopes=["openid", "profile", "email"]
)

# After user authorizes, exchange code for tokens
tokens = client.token("AUTHORIZATION_CODE", "http://localhost:8080/callback", code_verifier)

# Get user info
userinfo = client.userinfo(tokens['access_token'])

# Refresh token
new_tokens = client.refresh(tokens['refresh_token'])

Example cURL Commands

Complete Authorization Code Flow with PKCE

#!/bin/bash
set -e

BASE_URL="http://localhost:5000"
CLIENT_ID="your-client-id"
CLIENT_SECRET="your-client-secret"
EMAIL="user@example.com"
PASSWORD="UserPassword123!"
REDIRECT_URI="http://localhost:8080/callback"

echo "=== OIDC Authorization Code Flow ==="

# Step 1: Generate PKCE parameters
echo "1. Generating PKCE parameters..."
CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-' | cut -c1-43)
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl sha256 -binary | base64 | tr -d '=' | tr '/+' '_-')
STATE=$(openssl rand -hex 16)
NONCE=$(openssl rand -hex 16)
echo "   Code verifier: ${CODE_VERIFIER:0:20}..."
echo "   Code challenge: $CODE_CHALLENGE"

# Step 2: Get authorization code
echo "2. Getting authorization code..."
AUTH_RESPONSE=$(curl -s -D - -X POST "$BASE_URL/oidc/authorize" \
  -d "client_id=$CLIENT_ID" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "response_type=code" \
  -d "scope=openid profile email" \
  -d "state=$STATE" \
  -d "nonce=$NONCE" \
  -d "code_challenge=$CODE_CHALLENGE" \
  -d "code_challenge_method=S256" \
  -d "email=$EMAIL" \
  -d "password=$PASSWORD")

AUTH_CODE=$(echo "$AUTH_RESPONSE" | grep -i "Location:" | cut -d'?' -f2 | cut -d'=' -f2 | tr -d '\r')
echo "   Authorization code: ${AUTH_CODE:0:20}..."

# Step 3: Exchange code for tokens
echo "3. Exchanging code for tokens..."
TOKEN_RESPONSE=$(curl -s -X POST "$BASE_URL/oidc/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=$AUTH_CODE" \
  -d "redirect_uri=$REDIRECT_URI" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" \
  -d "code_verifier=$CODE_VERIFIER")

ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.data.access_token')
REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.data.refresh_token')
echo "   Access token received: ${ACCESS_TOKEN:0:20}..."

# Step 4: Get user info
echo "4. Getting user info..."
USERINFO=$(curl -s -X GET "$BASE_URL/oidc/userinfo" \
  -H "Authorization: Bearer $ACCESS_TOKEN")
echo "   User: $(echo "$USERINFO" | jq -r '.data.name')"

# Step 5: Introspect token
echo "5. Introspecting token..."
INTROSPECT=$(curl -s -X POST "$BASE_URL/oidc/introspect" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$ACCESS_TOKEN" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET")
echo "   Token active: $(echo "$INTROSPECT" | jq -r '.data.active')"

# Step 6: Refresh token
echo "6. Refreshing token..."
REFRESH_RESPONSE=$(curl -s -X POST "$BASE_URL/oidc/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=$REFRESH_TOKEN" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET")
echo "   Token refreshed successfully"

# Step 7: Revoke tokens
echo "7. Revoking tokens..."
curl -s -X POST "$BASE_URL/oidc/revoke" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "token=$REFRESH_TOKEN" \
  -d "token_type_hint=refresh_token" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" > /dev/null
echo "   Tokens revoked"

echo ""
echo "=== Flow Complete ==="

Security Considerations

PKCE Requirements

Proof Key for Code Exchange (PKCE) is strongly recommended for all clients, including confidential clients.

Why PKCE?

  • Protects against authorization code interception attacks
  • Required for public clients (SPA, mobile)
  • Recommended for all clients per OAuth 2.1

Implementation:

  1. Generate code_verifier (43-128 characters)
  2. Create code_challenge from verifier (SHA256)
  3. Send code_challenge and code_challenge_method in authorization request
  4. Send code_verifier in token request
import hashlib
import base64
import secrets

# Generate code verifier
code_verifier = secrets.token_urlsafe(43)

# Generate code challenge
code_challenge = base64.urlsafe_b64encode(
    hashlib.sha256(code_verifier.encode()).digest()
).decode().rstrip('=')

Token Lifetimes

Token Type Default Maximum Description
Access Token 3600s (1 hour) 86400s (24h) Short-lived token for API access
ID Token 3600s (1 hour) 86400s (24h) Identity token
Refresh Token 2592000s (30 days) 31536000s (1 year) Long-lived token for refresh

Configuration: Configure token lifetimes per client in the database or during registration.

Redirect URI Validation

Strict redirect URI validation is critical for security:

  1. Exact Matching: Use exact string matching (no wildcards)
  2. HTTPS Required: Require HTTPS in production
  3. No Wildcards: Never allow wildcards in domains
  4. Validate All URIs: Validate each registered redirect URI
  5. Case Sensitivity: Consider case sensitivity in path components

Example Validation:

from urllib.parse import urlparse

def validate_redirect_uri(uri):
    parsed = urlparse(uri)
    
    # Check for required components
    if not parsed.scheme or not parsed.netloc:
        raise ValueError("Invalid redirect URI: missing scheme or netloc")
    
    # Require HTTPS in production
    if parsed.scheme != 'https' and parsed.netloc not in ('localhost', '127.0.0.1'):
        raise ValueError("HTTPS required for redirect URI in production")
    
    # No fragments
    if parsed.fragment:
        raise ValueError("Redirect URI must not contain fragment")
    
    return True

Client Secrets Management

  1. Secure Storage: Store secrets in environment variables or secrets manager
  2. Hash Storage: Secrets are hashed (bcrypt) in the database
  3. Rotation: Support secret rotation without service interruption
  4. Scope: Limit client permissions to minimum required scopes

Environment Variables:

# Don't commit secrets to version control
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret

Additional Security Measures

  1. HTTPS/TLS: Always use HTTPS in production
  2. State Parameter: Always validate state parameter to prevent CSRF
  3. Nonce Validation: Validate nonce in ID token to prevent replay attacks
  4. Token Binding: Consider token binding for high-security scenarios
  5. Audit Logging: Enable comprehensive audit logging
  6. Rate Limiting: Implement rate limiting for all endpoints

Deployment Checklist

Environment Variables

# Required
DATABASE_URL=postgresql://user:pass@localhost:5432/authy2
SECRET_KEY=your-secure-secret-key-min-32-chars
OIDC_ISSUER_URL=https://your-oidc-provider.com

# Recommended
BCRYPT_LOG_ROUNDS=12
LOG_LEVEL=INFO
REDIS_URL=redis://localhost:6379/0

# Optional
CORS_ORIGINS=https://yourapp.com
RATELIMIT_ENABLED=true

Database Migrations

  1. Run migrations before deployment:
python manage.py db upgrade
  1. Verify migration:
python manage.py db current
python manage.py db history
  1. Backup database before migration:
pg_dump -h localhost -U postgres authy2 > backup.sql

SSL/TLS Requirements

Production Requirements:

  1. TLS 1.2+: Use TLS 1.2 or higher
  2. Valid Certificate: Use certificates from trusted CA
  3. HSTS Header: Enable HTTP Strict Transport Security
  4. No Mixed Content: Ensure all resources load over HTTPS

Example Nginx Configuration:

server {
    listen 443 ssl;
    server_name oidc.example.com;
    
    ssl_certificate /etc/ssl/certs/oidc.crt;
    ssl_certificate_key /etc/ssl/private/oidc.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    location / {
        proxy_pass http://127.0.0.1:5000;
        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 oidc.example.com;
    return 301 https://$host$request_uri;
}

Monitoring and Logging

Recommended Metrics:

  1. Token Issuance Rate: Tokens per minute/hour
  2. Error Rate: 4xx and 5xx response codes
  3. Token Validation Failures: Invalid token attempts
  4. Authorization Code Usage: Single-use validation
  5. Client Activity: Active clients and usage patterns

Log Format:

{
  "timestamp": "2024-01-01T00:00:00Z",
  "level": "INFO",
  "event_type": "token_issued",
  "client_id": "oidc_...",
  "user_id": "user-uuid",
  "scope": "openid profile email",
  "ip_address": "192.168.1.1",
  "request_id": "req-uuid"
}

Pre-Deployment Checklist

  • Database migrations applied
  • SSL/TLS certificates installed
  • Environment variables configured
  • Logging configured and tested
  • Monitoring/alerting set up
  • Backup procedures tested
  • Load balancing configured
  • Rate limiting enabled
  • CORS configured for allowed origins
  • Security headers enabled
  • Performance tested under load

Troubleshooting

Common Errors and Solutions

Error: invalid_client

Cause: Client authentication failed.

Solutions:

  1. Verify client_id and client_secret are correct
  2. Check if client is active (not disabled)
  3. Ensure client authentication method matches
# Test client authentication
curl -X POST http://localhost:5000/oidc/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET"

Error: invalid_grant

Cause: Authorization code is invalid, expired, or already used.

Solutions:

  1. Authorization codes expire after 10 minutes
  2. Each code can only be used once
  3. Ensure redirect_uri matches original request
# Check authorization code validity
# Codes expire quickly and are single-use

Error: invalid_request - code_verifier required

Cause: PKCE required but code_verifier not provided.

Solutions:

  1. Generate code verifier and challenge
  2. Include code_verifier in token request
  3. Ensure code_challenge_method is S256

Error: invalid_request - Invalid redirect_uri

Cause: Redirect URI doesn't match registered URIs.

Solutions:

  1. Verify exact redirect URI matches
  2. Check for trailing slashes or whitespace
  3. Ensure HTTPS in production
# Debug redirect URI validation
client = OIDCClient.query.filter_by(client_id=client_id).first()
allowed_uris = client.redirect_uris
is_valid = client.is_redirect_uri_allowed(redirect_uri)

Error: invalid_scope

Cause: Requested scope not allowed for client.

Solutions:

  1. Client must request only allowed scopes
  2. Check client configuration for allowed scopes
# Verify allowed scopes
client = OIDCClient.query.filter_by(client_id=client_id).first()
allowed_scopes = client.scopes  # ["openid", "profile", "email"]

Debug Logging

Enable Debug Logging:

export LOG_LEVEL=DEBUG

Example Log Output:

{
  "timestamp": "2024-01-01T00:00:00Z",
  "level": "DEBUG",
  "event_type": "authorization_code_issued",
  "message": "Authorization code generated",
  "client_id": "oidc_abc123",
  "user_id": "user-uuid",
  "scope": ["openid", "profile", "email"],
  "redirect_uri": "http://localhost:8080/callback",
  "code_challenge_method": "S256",
  "ip_address": "192.168.1.1",
  "request_id": "req-uuid"
}

Token Validation Issues

Token Expired

{
  "data": {
    "active": false
  }
}

Solution: Use refresh token to get new access token.

Invalid Signature

Cause: Token signed with different key.

Solutions:

  1. Fetch latest JWKS
  2. Verify key ID (kid) matches
  3. Check key rotation
import jwt

# Fetch JWKS
jwks = requests.get("http://localhost:5000/oidc/jwks").json()

# Get signing key
for key in jwks["keys"]:
    if key["kid"] == token_header["kid"]:
        public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
        break

Audience Mismatch

Cause: Token audience doesn't match client ID.

Solution: Ensure aud claim matches your client_id.

Database Issues

Connection Failed

# Test database connection
export DATABASE_URL="postgresql://user:pass@localhost:5432/authy2"
python -c "create_app create_app; app = create_app(); app.test_request_context().push()"

Migration Issues

# Check migration status
python manage.py db current

# Show migration history
python manage.py db history

# Stamp to specific version
python manage.py db stamp 001

Performance Issues

Slow Token Issuance

  1. Check database connection pooling
  2. Verify Redis connection (if used)
  3. Monitor database query performance
  4. Check for N+1 queries in token generation

High Memory Usage

  1. Monitor JWKS caching
  2. Check token metadata cleanup
  3. Verify audit log rotation

Getting Help

  1. Check Logs: Review application logs for detailed error messages
  2. Test Endpoints: Use docs/OIDC_TESTING.md for manual testing
  3. Verify Configuration: Check config/base.py for configuration options
  4. Run Tests: Execute test suite to verify functionality:
pytest tests/integration/test_oidc_flow.py -v


Version History

Version Date Changes
1.0.0 2024-01-01 Initial OIDC provider documentation