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