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
- Overview
- Quick Start
- API Endpoints Reference
- OIDC Client Configuration
- Integration Examples
- Security Considerations
- Deployment Checklist
- 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
- Python 3.9+ with pip
- PostgreSQL 13+ database
- Redis (optional, for session storage)
- OIDC Client Library for your platform
Installation
- Clone the repository and install dependencies:
git clone <repository-url>
cd backend
pip install -r requirements/base.txt
- Set up environment variables:
cp .env.example .env
# Edit .env with your configuration
- 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 clientsoidc_authorization_codes- Temporary authorization codesoidc_refresh_tokens- Refresh tokens with rotation supportoidc_sessions- OIDC session trackingoidc_token_metadata- Token metadata for revocationoidc_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- Success500- 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 URL200- 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 successfully400- Invalid request or grant401- Invalid client credentials500- 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 returned401- Invalid or expired token500- 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 returned500- 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 request401- Invalid client credentials500- 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 complete400- Invalid request401- Invalid client credentials500- 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_urismust contain valid URIs with scheme and netlocgrant_typesmust be a subset of["authorization_code", "refresh_token"]response_typesmust be a subset of["code"]scopemust be a subset of["openid", "profile", "email"]
Status Codes:
201- Client registered successfully400- Invalid request or validation error500- 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:
- Exact Matching: Redirect URIs are matched exactly (no wildcards)
- Scheme Required: Must have
http://,https://, or custom scheme - No Fragments: Fragment components (
#) are not allowed - 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:
- 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"
}'
- 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"
- 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:
- Generate
code_verifier(43-128 characters) - Create
code_challengefrom verifier (SHA256) - Send
code_challengeandcode_challenge_methodin authorization request - Send
code_verifierin 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:
- Exact Matching: Use exact string matching (no wildcards)
- HTTPS Required: Require HTTPS in production
- No Wildcards: Never allow wildcards in domains
- Validate All URIs: Validate each registered redirect URI
- 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
- Secure Storage: Store secrets in environment variables or secrets manager
- Hash Storage: Secrets are hashed (bcrypt) in the database
- Rotation: Support secret rotation without service interruption
- 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
- HTTPS/TLS: Always use HTTPS in production
- State Parameter: Always validate state parameter to prevent CSRF
- Nonce Validation: Validate nonce in ID token to prevent replay attacks
- Token Binding: Consider token binding for high-security scenarios
- Audit Logging: Enable comprehensive audit logging
- 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
- Run migrations before deployment:
python manage.py db upgrade
- Verify migration:
python manage.py db current
python manage.py db history
- Backup database before migration:
pg_dump -h localhost -U postgres authy2 > backup.sql
SSL/TLS Requirements
Production Requirements:
- TLS 1.2+: Use TLS 1.2 or higher
- Valid Certificate: Use certificates from trusted CA
- HSTS Header: Enable HTTP Strict Transport Security
- 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:
- Token Issuance Rate: Tokens per minute/hour
- Error Rate: 4xx and 5xx response codes
- Token Validation Failures: Invalid token attempts
- Authorization Code Usage: Single-use validation
- 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:
- Verify
client_idandclient_secretare correct - Check if client is active (not disabled)
- 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:
- Authorization codes expire after 10 minutes
- Each code can only be used once
- Ensure
redirect_urimatches 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:
- Generate code verifier and challenge
- Include
code_verifierin token request - Ensure
code_challenge_methodisS256
Error: invalid_request - Invalid redirect_uri
Cause: Redirect URI doesn't match registered URIs.
Solutions:
- Verify exact redirect URI matches
- Check for trailing slashes or whitespace
- 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:
- Client must request only allowed scopes
- 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:
- Fetch latest JWKS
- Verify key ID (kid) matches
- 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
- Check database connection pooling
- Verify Redis connection (if used)
- Monitor database query performance
- Check for N+1 queries in token generation
High Memory Usage
- Monitor JWKS caching
- Check token metadata cleanup
- Verify audit log rotation
Getting Help
- Check Logs: Review application logs for detailed error messages
- Test Endpoints: Use
docs/OIDC_TESTING.mdfor manual testing - Verify Configuration: Check
config/base.pyfor configuration options - Run Tests: Execute test suite to verify functionality:
pytest tests/integration/test_oidc_flow.py -v
Related Documentation
- Architecture Documentation - Overall system architecture
- OIDC Testing Guide - Manual testing procedures
- OAuth2-Proxy Configuration - Example oauth2-proxy config
- API Response Format - Standard response envelope
- Configuration Reference - Complete configuration options
Version History
| Version | Date | Changes |
|---|---|---|
| 1.0.0 | 2024-01-01 | Initial OIDC provider documentation |