407 lines
14 KiB
Markdown
407 lines
14 KiB
Markdown
# Authentik OIDC Integration Guide
|
|
|
|
## Overview
|
|
|
|
This document describes how Authentik (or any OIDC-compatible identity provider) is integrated with the Bill Tracker application for single sign-on (SSO) authentication.
|
|
|
|
## Architecture
|
|
|
|
### Components
|
|
|
|
| Component | File | Purpose |
|
|
|-----------|------|---------|
|
|
| **OIDC Routes** | `routes/authOidc.js` | Express routes for login initiation and callback handling |
|
|
| **OIDC Service** | `services/oidcService.js` | Core OIDC logic: token validation, user provisioning, group mapping |
|
|
| **CSRF Middleware** | `middleware/csrf.js` | CSRF protection for OIDC endpoints |
|
|
| **Auth Service** | `services/authService.js` | Session creation and management |
|
|
|
|
### Data Flow
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
|
│ Browser │ │ Bill │ │ Authentik │
|
|
│ │ │ Tracker │ │ (IdP) │
|
|
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
|
│ │ │
|
|
│ Click "Login" │ │
|
|
│───────────────────────>│ │
|
|
│ │ │
|
|
│ │ GET /api/auth/oidc/login │
|
|
│ │───────────────────────>│
|
|
│ │ │
|
|
│ │ │ Generate PKCE + State
|
|
│ │ │ Store in DB with TTL
|
|
│ │ │
|
|
│ │ │ Redirect to Authentik
|
|
│ │<───────────────────────│
|
|
│ │ │
|
|
│ │ 302 Redirect │
|
|
│ │───────────────────────>│
|
|
│ │ │
|
|
│ │ │ User authenticates
|
|
│ │ │
|
|
│ │ │ Redirect back with code
|
|
│ │ │ GET /api/auth/oidc/callback?code=...
|
|
│ │<───────────────────────│
|
|
│ │ │
|
|
│ │ Exchange code for tokens │
|
|
│ │ Verify ID token (JWKS) │
|
|
│ │ Validate state/PKCE │
|
|
│ │ │
|
|
│ │ Find/create user │
|
|
│ │ │
|
|
│ │ Create local session │
|
|
│ │ Set session cookie │
|
|
│ │ │
|
|
│ │ 302 Redirect to / │
|
|
│<───────────────────────│ │
|
|
│ │ │
|
|
│ Session cookie │ │
|
|
│ Authenticated │ │
|
|
│ │ │
|
|
```
|
|
|
|
## Environment Configuration
|
|
|
|
Add these to your `.env` file (or configure via Admin panel):
|
|
|
|
```bash
|
|
# OIDC Enabled
|
|
OIDC_ENABLED=true
|
|
|
|
# Authentik Provider Details
|
|
OIDC_ISSUER_URL=https://auth.yourdomain.com/application/o/bill-tracker/
|
|
OIDC_CLIENT_ID=your-client-id-from-authentik
|
|
OIDC_CLIENT_SECRET=your-client-secret-from-authentik
|
|
OIDC_REDIRECT_URI=https://bills.yourdomain.com/api/auth/oidc/callback
|
|
|
|
# Scopes to request
|
|
OIDC_SCOPES="openid email profile groups"
|
|
|
|
# Admin Group Mapping
|
|
OIDC_ADMIN_GROUP=bill-tracker-admins
|
|
|
|
# Optional
|
|
OIDC_AUTO_PROVISION=true
|
|
OIDC_DEFAULT_ROLE=user
|
|
OIDC_PROVIDER_NAME=authentik
|
|
```
|
|
|
|
## Authentik Setup
|
|
|
|
### Step 1: Create OAuth2/OpenID Provider
|
|
|
|
In Authentik, navigate to **Providers** → **Create**:
|
|
|
|
| Setting | Value |
|
|
|---------|-------|
|
|
| Name | `bill-tracker` |
|
|
| Client Type | `Confidential` |
|
|
| Authorization Flow | `Explicit` |
|
|
| Redirect URIs | `https://bills.yourdomain.com/api/auth/oidc/callback` |
|
|
| Scopes | `openid`, `email`, `profile`, `groups` |
|
|
|
|
### Step 2: Create Application
|
|
|
|
In Authentik, navigate to **Applications** → **Applications** → **Create**:
|
|
|
|
| Setting | Value |
|
|
|---------|-------|
|
|
| Name | `bill-tracker` |
|
|
| Provider | Select the `bill-tracker` provider created above |
|
|
| Slug | `bill-tracker` |
|
|
| Path | `/` (or appropriate path) |
|
|
|
|
### Step 3: Assign Users
|
|
|
|
Assign users or groups to the `bill-tracker` application in Authentik.
|
|
|
|
## Configuration Options
|
|
|
|
### Priority Order
|
|
|
|
Configuration is resolved in this order:
|
|
|
|
1. **Database settings** (via Admin panel) — Highest priority
|
|
2. **Environment variables** — Fallback if DB value is blank
|
|
3. **Safe defaults** — If neither DB nor env is set
|
|
|
|
### Required Settings
|
|
|
|
| Setting | Env Variable | Description |
|
|
|---------|--------------|-------------|
|
|
| Issuer URL | `OIDC_ISSUER_URL` | Authentik application issuer URL (includes `/application/o/` path) |
|
|
| Client ID | `OIDC_CLIENT_ID` | Client ID from Authentik application |
|
|
| Client Secret | `OIDC_CLIENT_SECRET` | Client secret from Authentik application |
|
|
| Redirect URI | `OIDC_REDIRECT_URI` | Must exactly match Authentik redirect URI |
|
|
|
|
### Optional Settings
|
|
|
|
| Setting | Env Variable | Default | Description |
|
|
|---------|--------------|---------|-------------|
|
|
| Scopes | `OIDC_SCOPES` | `openid email profile groups` | Space-separated list of OAuth2 scopes |
|
|
| Admin Group | `OIDC_ADMIN_GROUP` | (none) | Authentik group name whose members get admin role |
|
|
| Auto Provision | `OIDC_AUTO_PROVISION` | `true` | Auto-create users if they don't exist |
|
|
| Default Role | (DB only) | `user` | Role for non-admin users |
|
|
| Token Auth Method | `OIDC_TOKEN_AUTH_METHOD` | `client_secret_basic` | How client authenticates to token endpoint |
|
|
|
|
## User Provisioning
|
|
|
|
### Auto-Provision Flow
|
|
|
|
When a user logs in via OIDC and doesn't exist locally:
|
|
|
|
1. **User is found or created** by `external_subject` (from OIDC `sub` claim)
|
|
2. **Email matching** — If no user by `sub`, check for existing user with same email (if `email_verified=true`)
|
|
3. **Provisioning** — If `OIDC_AUTO_PROVISION=true`, create new user with:
|
|
- Role: `admin` if in configured admin group, else `user`
|
|
- Password: empty (cannot use local password login)
|
|
- `auth_provider`: `oidc`
|
|
- `external_subject`: OIDC `sub` claim
|
|
|
|
### Group → Role Mapping
|
|
|
|
```javascript
|
|
// Pseudocode
|
|
function mapRoleFromClaims(claims, config) {
|
|
const adminGroup = config.adminGroup;
|
|
const groups = claims.groups || [];
|
|
|
|
if (!adminGroup) return 'user';
|
|
|
|
if (Array.isArray(groups) && groups.includes(adminGroup)) {
|
|
return 'admin';
|
|
}
|
|
|
|
return 'user';
|
|
}
|
|
```
|
|
|
|
## Security Features
|
|
|
|
### PKCE (Proof Key for Code Exchange)
|
|
|
|
Prevents code interception attacks:
|
|
|
|
1. Client generates `code_verifier` (random string)
|
|
2. Client creates `code_challenge = SHA256(code_verifier)`
|
|
3. Authorization request includes `code_challenge`
|
|
4. Token exchange includes `code_verifier`
|
|
5. Server verifies `SHA256(code_verifier) == code_challenge`
|
|
|
|
### State Parameter
|
|
|
|
Prevents CSRF on OAuth flow:
|
|
|
|
1. Random `state` is generated and stored in DB (5-minute TTL)
|
|
2. User redirected to Authentik with `state` parameter
|
|
3. On callback, state is validated and immediately consumed (prevents replay)
|
|
4. If state doesn't match or is expired → redirect with error
|
|
|
|
### ID Token Verification
|
|
|
|
Using `openid-client@5`, the following validations are performed:
|
|
|
|
| Check | Purpose |
|
|
|-------|---------|
|
|
| JWT signature via JWKS | Cryptographic verification of issuer |
|
|
| Issuer (`iss`) | Must match configured issuer URL |
|
|
| Audience (`aud`) | Must include client ID |
|
|
| Expiry (`exp`) | Token must not be expired |
|
|
| Not-before (`nbf`) | Token must not be used before date |
|
|
| Nonce | Prevents replay attacks |
|
|
| `sub` claim | User identifier must be present |
|
|
|
|
### Security Best Practices
|
|
|
|
- Tokens and codes are **never logged**
|
|
- Client secret is **never exposed** to frontend
|
|
- State tokens are **consumed immediately** (one-time use)
|
|
- Session cookies use same security settings as local login
|
|
- Admin group mapping requires explicit group membership
|
|
|
|
## Troubleshooting
|
|
|
|
### Issue: "OIDC authentication is not configured"
|
|
|
|
**Symptoms:**
|
|
- Login redirects return 501
|
|
- `isOidcLoginActive()` returns `false`
|
|
|
|
**Check:**
|
|
1. Verify all required environment variables are set
|
|
2. Check Admin panel → OIDC configuration
|
|
3. Verify `oidc_login_enabled` setting is `true`
|
|
4. Test configuration via Admin panel → "Test Configuration" button
|
|
|
|
### Issue: "Failed to reach the identity provider"
|
|
|
|
**Symptoms:**
|
|
- Login redirects return 502
|
|
- Network error in server logs
|
|
|
|
**Check:**
|
|
1. Verify `OIDC_ISSUER_URL` points to the **issuer**, not `/authorize/` endpoint
|
|
2. Test issuer discovery: `curl -v <OIDC_ISSUER_URL>/.well-known/openid-configuration`
|
|
3. Check network connectivity from server to Authentik
|
|
4. Verify TLS/SSL certificates are valid
|
|
|
|
### Issue: "Invalid or expired state"
|
|
|
|
**Symptoms:**
|
|
- Callback redirects with `oidc_error=invalid_or_expired_state`
|
|
- State parameter mismatch
|
|
|
|
**Check:**
|
|
1. Clear browser cookies (including Authentik session)
|
|
2. Ensure only one Authentik login flow is active per browser
|
|
3. Check server logs for state creation/consumption timing
|
|
|
|
### Issue: "access_denied" or "authentication_failed"
|
|
|
|
**Symptoms:**
|
|
- Callback redirects with error query parameter
|
|
- User redirected to login with error message
|
|
|
|
**Common causes:**
|
|
- User not assigned to application in Authentik
|
|
- Groups claim missing expected admin group
|
|
- Token expired before exchange
|
|
- PKCE validation failed (replay attempt)
|
|
|
|
### Issue: Email not linking to existing account
|
|
|
|
**Symptoms:**
|
|
- New user created instead of linking existing local account
|
|
|
|
**Check:**
|
|
1. Authentik must send `email_verified=true` in claims
|
|
2. Existing local user must have `auth_provider='local'`
|
|
3. Only first match is linked (create one local account per email)
|
|
|
|
## API Endpoints
|
|
|
|
### GET /api/auth/oidc/login
|
|
|
|
Initiates OIDC login flow.
|
|
|
|
**Query Parameters:**
|
|
- `redirect_to` (optional): Path to redirect after successful login
|
|
|
|
**Behavior:**
|
|
- If OIDC not configured → returns 501
|
|
- Redirects to Authentik authorization endpoint
|
|
- State stored in DB with 5-minute TTL
|
|
|
|
**Success Response:** 302 Redirect to Authentik
|
|
|
|
### GET /api/auth/oidc/callback
|
|
|
|
Handles redirect from Authentik after user authentication.
|
|
|
|
**Query Parameters:**
|
|
- `code`: Authorization code from Authentik
|
|
- `state`: State parameter for CSRF protection
|
|
- `error` (optional): Provider error code
|
|
|
|
**Behavior:**
|
|
- Validates and consumes state
|
|
- Exchanges code for tokens
|
|
- Verifies ID token (signature, claims)
|
|
- Finds or creates local user
|
|
- Creates local session
|
|
- Sets session cookie
|
|
- Redirects to frontend
|
|
|
|
**Error Redirects:**
|
|
- `oidc_error=not_configured`: OIDC not enabled
|
|
- `oidc_error=authorization_failed`: Authentik error
|
|
- `oidc_error=invalid_callback`: Missing code or state
|
|
- `oidc_error=invalid_or_expired_state`: State mismatch/expired
|
|
- `oidc_error=authentication_failed`: Token validation failed
|
|
- `oidc_error=access_denied`: User denied access
|
|
|
|
**Success Response:** 302 Redirect to `/` or `redirect_to` path
|
|
|
|
## Admin Panel
|
|
|
|
### OIDC Settings Location
|
|
|
|
**Admin Panel** → **Authentication** → **OIDC Settings**
|
|
|
|
### Settings Form Fields
|
|
|
|
| Field | Source | Description |
|
|
|-------|--------|-------------|
|
|
| Provider Name | Env/Default | Display name for login button |
|
|
| Issuer URL | Env/DB | Authentik issuer URL |
|
|
| Client ID | Env/DB | OAuth2 client ID |
|
|
| Client Secret | Env/DB | OAuth2 client secret (masked) |
|
|
| Token Auth Method | Env/DB | `client_secret_basic` or `client_secret_post` |
|
|
| Redirect URI | Auto | Must match Authentik (auto-populated) |
|
|
| Scopes | Env/DB | Space-separated scopes |
|
|
| Admin Group | Env/DB | Authentik group name for admin role |
|
|
| Auto Provision | Env/DB | Create users automatically |
|
|
| Enabled | DB only | Toggle OIDC login |
|
|
|
|
### Testing Configuration
|
|
|
|
Click **"Test Configuration"** to:
|
|
|
|
1. Discover OIDC metadata from issuer URL
|
|
2. Verify authorization, token, and JWKS endpoints exist
|
|
3. Validate client credentials
|
|
|
|
**Response includes:**
|
|
- Configuration status (ok/error)
|
|
- Missing fields if error
|
|
- Provider metadata (issuer, scopes, etc.)
|
|
|
|
## Advanced Topics
|
|
|
|
### JWKS Key Rotation
|
|
|
|
The OIDC client cache has a 1-hour TTL:
|
|
|
|
```javascript
|
|
const CLIENT_CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
```
|
|
|
|
When Authentik rotates keys (via JWKS), the next token exchange will:
|
|
1. Detect cache expiration
|
|
2. Re-discover OIDC provider
|
|
3. Fetch new JWKS
|
|
4. Verify token signature with new key
|
|
|
|
### Multiple OIDC Providers
|
|
|
|
Not currently supported. The system uses a single OIDC configuration. Workarounds:
|
|
- Use Authentik as a single identity provider aggregating multiple backends
|
|
- Deploy separate instances per provider
|
|
|
|
### Custom Claim Mapping
|
|
|
|
To add custom role mapping, modify `mapRoleFromClaims()` in `services/oidcService.js`:
|
|
|
|
```javascript
|
|
function mapRoleFromClaims(claims, config) {
|
|
// Custom logic here
|
|
if (claims.your_custom_claim === 'value') {
|
|
return 'custom_role';
|
|
}
|
|
|
|
// Default logic
|
|
const adminGroup = config.adminGroup;
|
|
const groups = claims.groups || [];
|
|
return list.includes(adminGroup) ? 'admin' : 'user';
|
|
}
|
|
```
|
|
|
|
## References
|
|
|
|
- [`routes/authOidc.js`](../routes/authOidc.js) — OIDC Express routes
|
|
- [`services/oidcService.js`](../services/oidcService.js) — OIDC service logic
|
|
- [`middleware/csrf.js`](../middleware/csrf.js) — CSRF middleware for OIDC
|
|
- [openid-client@5 Documentation](https://github.com/panva/node-openid-client)
|
|
- [OWASP OIDC Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OpenID_Connect_Cheat_Sheet.html)
|