first commit
This commit is contained in:
622
docs/API_GUIDE.md
Normal file
622
docs/API_GUIDE.md
Normal file
@ -0,0 +1,622 @@
|
||||
# API Integration Guide
|
||||
|
||||
Complete guide for developers integrating applications with this OIDC Identity Provider.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Getting Started](#getting-started)
|
||||
3. [OIDC Flow](#oidc-flow)
|
||||
4. [Endpoints Reference](#endpoints-reference)
|
||||
5. [Client Configuration](#client-configuration)
|
||||
6. [Code Examples](#code-examples)
|
||||
7. [Testing](#testing)
|
||||
8. [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This OIDC Identity Provider implements the **Authorization Code Flow**, which is the most secure OAuth 2.0 / OpenID Connect flow suitable for server-side applications.
|
||||
|
||||
### What You Get
|
||||
|
||||
- **User Authentication**: Delegate authentication to this IdP
|
||||
- **User Information**: Retrieve user profile (email, name, etc.)
|
||||
- **Single Sign-On (SSO)**: Users log in once, access multiple applications
|
||||
- **Secure Tokens**: RS256-signed ID tokens and access tokens
|
||||
|
||||
### Supported Grant Types
|
||||
|
||||
- ✅ Authorization Code Flow (recommended)
|
||||
- ❌ Implicit Flow (not supported - insecure)
|
||||
- ❌ Client Credentials (not yet implemented)
|
||||
- ❌ Refresh Tokens (not yet implemented)
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. **OIDC Provider Running**: Deploy this IdP (see [Deployment Guide](deployment.md))
|
||||
2. **Admin Access**: You need admin credentials to register your application
|
||||
3. **HTTPS (Production)**: Required for secure cookie handling
|
||||
|
||||
### Step 1: Register Your Application
|
||||
|
||||
1. Navigate to the admin panel: `https://your-idp.com/admin/login`
|
||||
2. Log in with admin credentials
|
||||
3. Go to **Clients** → **Create New Client**
|
||||
4. Fill in the form:
|
||||
- **Client Name**: Your application name (e.g., "My Web App")
|
||||
- **Redirect URIs**: Where users return after login (e.g., `https://myapp.com/callback`)
|
||||
- **Allowed Scopes**: `openid profile email`
|
||||
|
||||
5. **Save the credentials**:
|
||||
```
|
||||
Client ID: abc123...
|
||||
Client Secret: xyz789... (shown only once!)
|
||||
```
|
||||
|
||||
### Step 2: Discover OIDC Configuration
|
||||
|
||||
Fetch the OIDC discovery document:
|
||||
|
||||
```bash
|
||||
curl https://your-idp.com/.well-known/openid-configuration
|
||||
```
|
||||
|
||||
This returns all endpoint URLs and supported features.
|
||||
|
||||
---
|
||||
|
||||
## OIDC Flow
|
||||
|
||||
### Authorization Code Flow (Step by Step)
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────────┐
|
||||
│ User │ │ Your App │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ │
|
||||
│ 1. Click "Login" │
|
||||
│───────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ 2. Redirect to /authorize │
|
||||
│<───────────────────────────────────────────────────│
|
||||
│ │
|
||||
┌────┴────┐ ┌─────┴───────┐
|
||||
│ User │ │ OIDC IdP │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ │
|
||||
│ 3. Login form shown │
|
||||
│<───────────────────────────────────────────────────│
|
||||
│ │
|
||||
│ 4. Submit credentials │
|
||||
│───────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ 5. Redirect to callback with code │
|
||||
│<───────────────────────────────────────────────────│
|
||||
│ │
|
||||
┌────┴────┐ ┌─────┴───────┐
|
||||
│ User │ │ Your App │
|
||||
└────┬────┘ └──────┬──────┘
|
||||
│ 6. Return to app │
|
||||
│───────────────────────────────────────────────────>│
|
||||
│ │
|
||||
│ ┌──────┴──────┐
|
||||
│ │ OIDC IdP │
|
||||
│ └──────┬──────┘
|
||||
│ │
|
||||
│ 7. Exchange code for tokens │
|
||||
│ <─────────────────────────────│
|
||||
│ │
|
||||
│ 8. Return tokens │
|
||||
│ ─────────────────────────────>│
|
||||
│ │
|
||||
│ 9. Logged in! │
|
||||
│<───────────────────────────────────────────────────│
|
||||
│ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Endpoints Reference
|
||||
|
||||
### 1. Discovery Endpoint
|
||||
|
||||
**Get OIDC Configuration**
|
||||
|
||||
```http
|
||||
GET /.well-known/openid-configuration
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"issuer": "https://your-idp.com",
|
||||
"authorization_endpoint": "https://your-idp.com/authorize",
|
||||
"token_endpoint": "https://your-idp.com/token",
|
||||
"userinfo_endpoint": "https://your-idp.com/userinfo",
|
||||
"jwks_uri": "https://your-idp.com/jwks",
|
||||
"response_types_supported": ["code"],
|
||||
"id_token_signing_alg_values_supported": ["RS256"],
|
||||
"scopes_supported": ["openid", "profile", "email"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Authorization Endpoint
|
||||
|
||||
**Initiate Login Flow**
|
||||
|
||||
```http
|
||||
GET /authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&scope=openid%20profile%20email&state={STATE}
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `client_id` | ✅ Yes | Your client ID |
|
||||
| `redirect_uri` | ✅ Yes | Where to redirect after login (must match registered URI) |
|
||||
| `response_type` | ✅ Yes | Must be `code` |
|
||||
| `scope` | ✅ Yes | Space-separated scopes (must include `openid`) |
|
||||
| `state` | ⚠️ Recommended | CSRF protection token (you generate this) |
|
||||
|
||||
**Example:**
|
||||
```
|
||||
https://your-idp.com/authorize?
|
||||
client_id=abc123&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
response_type=code&
|
||||
scope=openid%20profile%20email&
|
||||
state=random_csrf_token_123
|
||||
```
|
||||
|
||||
**Response:**
|
||||
User is redirected to login page. After successful login, redirected to:
|
||||
```
|
||||
https://myapp.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token_123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Token Endpoint
|
||||
|
||||
**Exchange Authorization Code for Tokens**
|
||||
|
||||
```http
|
||||
POST /token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
grant_type=authorization_code&
|
||||
code=AUTH_CODE&
|
||||
redirect_uri=https://myapp.com/callback&
|
||||
client_id=abc123&
|
||||
client_secret=xyz789
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-----------|----------|-------------|
|
||||
| `grant_type` | ✅ Yes | Must be `authorization_code` |
|
||||
| `code` | ✅ Yes | Authorization code from callback |
|
||||
| `redirect_uri` | ✅ Yes | Same URI used in authorization request |
|
||||
| `client_id` | ✅ Yes | Your client ID |
|
||||
| `client_secret` | ✅ Yes | Your client secret |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGci...",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"id_token": "eyJhbGci...",
|
||||
"scope": "openid profile email"
|
||||
}
|
||||
```
|
||||
|
||||
**ID Token Contents (JWT):**
|
||||
```json
|
||||
{
|
||||
"iss": "https://your-idp.com",
|
||||
"sub": "user-123",
|
||||
"aud": "abc123",
|
||||
"exp": 1234567890,
|
||||
"iat": 1234567890,
|
||||
"email": "user@example.com",
|
||||
"name": "John Doe",
|
||||
"preferred_username": "john"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. UserInfo Endpoint
|
||||
|
||||
**Get User Information**
|
||||
|
||||
```http
|
||||
GET /userinfo
|
||||
Authorization: Bearer {ACCESS_TOKEN}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"sub": "user-123",
|
||||
"username": "john",
|
||||
"email": "john@example.com",
|
||||
"name": "John Doe",
|
||||
"preferred_username": "john",
|
||||
"role": "user",
|
||||
"permissions": ["read:data"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. JWKS Endpoint
|
||||
|
||||
**Get Public Keys for Token Verification**
|
||||
|
||||
```http
|
||||
GET /jwks
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"keys": [
|
||||
{
|
||||
"kty": "RSA",
|
||||
"use": "sig",
|
||||
"kid": "...",
|
||||
"n": "...",
|
||||
"e": "AQAB"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Client Configuration
|
||||
|
||||
### Redirect URI Rules
|
||||
|
||||
✅ **Allowed:**
|
||||
- `https://myapp.com/callback`
|
||||
- `http://localhost:8080/callback` (development only)
|
||||
- `https://myapp.com/auth/oidc/callback`
|
||||
|
||||
❌ **Not Allowed:**
|
||||
- Wildcard URIs (`https://*.myapp.com/callback`)
|
||||
- Non-HTTP(S) schemes (`myapp://callback`)
|
||||
|
||||
### Scopes
|
||||
|
||||
| Scope | Description | User Info Included |
|
||||
|-------|-------------|--------------------|
|
||||
| `openid` | **Required** - Enables OIDC | `sub` |
|
||||
| `profile` | User profile information | `name`, `preferred_username` |
|
||||
| `email` | User email address | `email` |
|
||||
|
||||
---
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Python (Flask + Authlib)
|
||||
|
||||
```python
|
||||
from flask import Flask, redirect, url_for, session
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'your-secret-key'
|
||||
|
||||
oauth = OAuth(app)
|
||||
oauth.register(
|
||||
name='oidc',
|
||||
client_id='YOUR_CLIENT_ID',
|
||||
client_secret='YOUR_CLIENT_SECRET',
|
||||
server_metadata_url='https://your-idp.com/.well-known/openid-configuration',
|
||||
client_kwargs={'scope': 'openid profile email'}
|
||||
)
|
||||
|
||||
@app.route('/login')
|
||||
def login():
|
||||
redirect_uri = url_for('callback', _external=True)
|
||||
return oauth.oidc.authorize_redirect(redirect_uri)
|
||||
|
||||
@app.route('/callback')
|
||||
def callback():
|
||||
token = oauth.oidc.authorize_access_token()
|
||||
user_info = token['userinfo']
|
||||
session['user'] = user_info
|
||||
return redirect('/')
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
user = session.get('user')
|
||||
if user:
|
||||
return f"Hello, {user['name']}!"
|
||||
return '<a href="/login">Login</a>'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node.js (Express + Passport)
|
||||
|
||||
```javascript
|
||||
const express = require('express');
|
||||
const passport = require('passport');
|
||||
const { Strategy } = require('openid-client');
|
||||
const { Issuer } = require('openid-client');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Discover OIDC provider
|
||||
Issuer.discover('https://your-idp.com/.well-known/openid-configuration')
|
||||
.then(issuer => {
|
||||
const client = new issuer.Client({
|
||||
client_id: 'YOUR_CLIENT_ID',
|
||||
client_secret: 'YOUR_CLIENT_SECRET',
|
||||
redirect_uris: ['http://localhost:3000/callback'],
|
||||
response_types: ['code'],
|
||||
});
|
||||
|
||||
passport.use('oidc', new Strategy({ client }, (tokenSet, userinfo, done) => {
|
||||
return done(null, userinfo);
|
||||
}));
|
||||
|
||||
app.get('/login', passport.authenticate('oidc'));
|
||||
|
||||
app.get('/callback',
|
||||
passport.authenticate('oidc', { successRedirect: '/', failureRedirect: '/login' })
|
||||
);
|
||||
});
|
||||
|
||||
app.listen(3000);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### PHP (Laravel Socialite)
|
||||
|
||||
```php
|
||||
// config/services.php
|
||||
'oidc' => [
|
||||
'client_id' => env('OIDC_CLIENT_ID'),
|
||||
'client_secret' => env('OIDC_CLIENT_SECRET'),
|
||||
'redirect' => env('OIDC_REDIRECT_URI'),
|
||||
'base_url' => env('OIDC_ISSUER'),
|
||||
],
|
||||
|
||||
// routes/web.php
|
||||
Route::get('/login', function () {
|
||||
return Socialite::driver('oidc')->redirect();
|
||||
});
|
||||
|
||||
Route::get('/callback', function () {
|
||||
$user = Socialite::driver('oidc')->user();
|
||||
|
||||
// $user->name
|
||||
// $user->email
|
||||
// $user->token (access token)
|
||||
|
||||
Auth::login($user);
|
||||
return redirect('/dashboard');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### JavaScript (SPA - NOT RECOMMENDED)
|
||||
|
||||
⚠️ **Warning**: Authorization Code Flow requires a backend to keep the client secret secure. For SPAs, consider using **PKCE** (not yet implemented) or a backend-for-frontend (BFF) pattern.
|
||||
|
||||
**BFF Pattern (Recommended for SPAs):**
|
||||
```
|
||||
[React/Vue App] <--> [Your Node.js Backend] <--> [OIDC IdP]
|
||||
(handles OIDC flow)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing with cURL
|
||||
|
||||
**Step 1: Get Authorization Code**
|
||||
|
||||
Open in browser:
|
||||
```
|
||||
https://your-idp.com/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code&scope=openid%20profile%20email&state=test123
|
||||
```
|
||||
|
||||
After login, you'll be redirected to:
|
||||
```
|
||||
http://localhost:8080/callback?code=ABC123&state=test123
|
||||
```
|
||||
|
||||
**Step 2: Exchange Code for Token**
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-idp.com/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "grant_type=authorization_code" \
|
||||
-d "code=ABC123" \
|
||||
-d "redirect_uri=http://localhost:8080/callback" \
|
||||
-d "client_id=test-client" \
|
||||
-d "client_secret=YOUR_SECRET"
|
||||
```
|
||||
|
||||
**Step 3: Get User Info**
|
||||
|
||||
```bash
|
||||
curl https://your-idp.com/userinfo \
|
||||
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Using the Test Client
|
||||
|
||||
This repository includes a test client:
|
||||
|
||||
```bash
|
||||
# Start the test client
|
||||
python3 test_client.py
|
||||
|
||||
# Open browser
|
||||
open http://localhost:8080
|
||||
|
||||
# Click "Login with OIDC"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Errors
|
||||
|
||||
#### `invalid_redirect_uri`
|
||||
|
||||
**Problem**: The redirect URI doesn't match registered URIs.
|
||||
|
||||
**Solution**:
|
||||
1. Check admin panel → your client → registered redirect URIs
|
||||
2. Ensure exact match (including trailing slash)
|
||||
3. Use URL encoding for query parameters
|
||||
|
||||
#### `invalid_client`
|
||||
|
||||
**Problem**: Client ID or secret is incorrect.
|
||||
|
||||
**Solution**:
|
||||
1. Verify client ID and secret from admin panel
|
||||
2. Check for typos or whitespace
|
||||
3. Ensure client exists and is active
|
||||
|
||||
#### `invalid_grant`
|
||||
|
||||
**Problem**: Authorization code is invalid, expired, or already used.
|
||||
|
||||
**Solution**:
|
||||
1. Authorization codes expire in 10 minutes
|
||||
2. Codes can only be used once
|
||||
3. Restart the flow if code expired
|
||||
|
||||
#### `access_denied`
|
||||
|
||||
**Problem**: User denied authorization or login failed.
|
||||
|
||||
**Solution**:
|
||||
1. User may have clicked "Cancel"
|
||||
2. Check credentials
|
||||
3. Verify user account is active
|
||||
|
||||
---
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
**1. Check Discovery Document**
|
||||
```bash
|
||||
curl https://your-idp.com/.well-known/openid-configuration | jq
|
||||
```
|
||||
|
||||
**2. Validate ID Token**
|
||||
Use [jwt.io](https://jwt.io) to decode and verify the token structure.
|
||||
|
||||
**3. Enable Debug Logging**
|
||||
Set `LOG_LEVEL=DEBUG` in your application to see detailed OIDC flow logs.
|
||||
|
||||
**4. Check Server Logs**
|
||||
```bash
|
||||
docker-compose -f docker-compose.prod.yml logs -f oidc_server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### ✅ Do This
|
||||
|
||||
- ✅ **Use HTTPS** in production (required for secure cookies)
|
||||
- ✅ **Validate `state` parameter** to prevent CSRF attacks
|
||||
- ✅ **Store client secret securely** (environment variables, not in code)
|
||||
- ✅ **Validate ID token signature** using JWKS endpoint
|
||||
- ✅ **Check token expiration** (`exp` claim)
|
||||
- ✅ **Use short-lived access tokens** (default: 1 hour)
|
||||
- ✅ **Implement token refresh** (when available)
|
||||
|
||||
### ❌ Don't Do This
|
||||
|
||||
- ❌ **Don't use Implicit Flow** (insecure, deprecated)
|
||||
- ❌ **Don't store tokens in localStorage** (use httpOnly cookies or sessionStorage)
|
||||
- ❌ **Don't expose client secret** in frontend code
|
||||
- ❌ **Don't skip `state` parameter** validation
|
||||
- ❌ **Don't accept tokens without verification**
|
||||
- ❌ **Don't use HTTP** in production
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
Sensitive endpoints are rate-limited:
|
||||
|
||||
| Endpoint | Limit |
|
||||
|----------|-------|
|
||||
| `/token` | 10 requests per minute |
|
||||
| `/login` | 5 requests per minute |
|
||||
| `/register` | 3 requests per hour |
|
||||
|
||||
**Response when rate-limited:**
|
||||
```
|
||||
HTTP 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Documentation**: [docs/](../docs/)
|
||||
- **Architecture**: [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- **Deployment**: [deployment.md](deployment.md)
|
||||
- **Issues**: Report bugs via GitHub issues
|
||||
|
||||
---
|
||||
|
||||
## Appendix
|
||||
|
||||
### Token Lifetimes
|
||||
|
||||
| Token Type | Default Lifetime | Configurable |
|
||||
|------------|------------------|--------------|
|
||||
| Authorization Code | 10 minutes | `AUTHORIZATION_CODE_LIFETIME` |
|
||||
| Access Token | 1 hour | `ACCESS_TOKEN_LIFETIME` |
|
||||
| ID Token | 1 hour | `ID_TOKEN_LIFETIME` |
|
||||
|
||||
### Supported Claims
|
||||
|
||||
| Claim | Description | Scope Required |
|
||||
|-------|-------------|----------------|
|
||||
| `sub` | User ID (unique identifier) | `openid` |
|
||||
| `email` | User email address | `email` |
|
||||
| `name` | User full name | `profile` |
|
||||
| `preferred_username` | Display username | `profile` |
|
||||
| `role` | User role | `openid` |
|
||||
| `permissions` | User permissions array | `openid` |
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-28
|
||||
**Version**: 1.0.0
|
||||
Reference in New Issue
Block a user