# 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 /.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)