# 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](#overview) 2. [Quick Start](#quick-start) 3. [API Endpoints Reference](#api-endpoints-reference) 4. [OIDC Client Configuration](#oidc-client-configuration) 5. [Integration Examples](#integration-examples) 6. [Security Considerations](#security-considerations) 7. [Deployment Checklist](#deployment-checklist) 8. [Troubleshooting](#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: ```bash git clone cd backend pip install -r requirements/base.txt ``` 2. Set up environment variables: ```bash cp .env.example .env # Edit .env with your configuration ``` 3. Run database migrations: ```bash 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: ```bash # 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: ```bash 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:** ```json { "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`](docs/architecture.md). ### 1. Discovery Endpoint **URL:** `GET /.well-known/openid-configuration` Returns the OIDC provider configuration as JSON. **Request:** ```bash curl http://localhost:5000/.well-known/openid-configuration ``` **Response:** ```json { "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):** ```bash 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 HTTP/1.1 302 Found Location: http://localhost:8080/callback?code=AUTHORIZATION_CODE&state=YOUR_STATE ``` **Error Response (302 Redirect with Error):** ```http 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):** ```bash 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):** ```bash 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:** ```json { "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:** ```json { "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:** ```bash curl -X GET http://localhost:5000/oidc/userinfo \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` **Response:** ```json { "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:** ```bash curl http://localhost:5000/oidc/jwks ``` **Response:** ```json { "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:** ```bash 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:** ```json { "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:** ```bash 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):** ```json { "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):** ```json { "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:** ```bash 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):** ```json { "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 ```bash # 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`](docs/oauth2-proxy-config.yaml) for complete configuration. **Quick Setup:** 1. Register an OIDC client: ```bash 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" }' ``` 2. Create `oauth2-proxy.yaml`: ```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" ``` 3. Start oauth2-proxy: ```bash oauth2-proxy -config oauth2-proxy.yaml ``` ### Generic OIDC Client Integration #### Python Example ```python 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 ```bash #!/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 ```python 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:** ```python 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:** ```bash # 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 ```bash # 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:** ```bash python manage.py db upgrade ``` 2. **Verify migration:** ```bash python manage.py db current python manage.py db history ``` 3. **Backup database before migration:** ```bash 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:** ```nginx 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:** ```json { "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 ```bash # 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 ```bash # 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 ```python # 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 ```python # 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:** ```bash export LOG_LEVEL=DEBUG ``` **Example Log Output:** ```json { "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 ```json { "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 ```python 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 ```bash # Test database connection export DATABASE_URL="postgresql://user:pass@localhost:5432/authy2" python -c "from app import create_app; app = create_app(); app.test_request_context().push()" ``` #### Migration Issues ```bash # 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`](docs/OIDC_TESTING.md) for manual testing 3. **Verify Configuration**: Check [`config/base.py`](config/base.py) for configuration options 4. **Run Tests**: Execute test suite to verify functionality: ```bash pytest tests/integration/test_oidc_flow.py -v ``` --- ## Related Documentation - [Architecture Documentation](docs/architecture.md) - Overall system architecture - [OIDC Testing Guide](docs/OIDC_TESTING.md) - Manual testing procedures - [OAuth2-Proxy Configuration](docs/oauth2-proxy-config.yaml) - Example oauth2-proxy config - [API Response Format](docs/architecture.md#api-response-format) - Standard response envelope - [Configuration Reference](config/base.py) - Complete configuration options --- ## Version History | Version | Date | Changes | |---------|------|---------| | 1.0.0 | 2024-01-01 | Initial OIDC provider documentation |