BillTracker/docs/Authentik-Integration.md

14 KiB

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):

# 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 ProvidersCreate:

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 ApplicationsApplicationsCreate:

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

// 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 PanelAuthenticationOIDC 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:

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:

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