623 lines
17 KiB
Markdown
623 lines
17 KiB
Markdown
# 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
|