1517 lines
43 KiB
Markdown
1517 lines
43 KiB
Markdown
|
|
# 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 <repository-url>
|
||
|
|
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 |
|
||
|
|
|