Files
headquarter/docs/superpowers/specs/2026-05-17-auth-oauth-design.md
T

4.9 KiB

Auth OAuth Design

Goal

Implement OAuth2/OIDC authentication via Authentik with internal JWT access tokens, DB-backed refresh token rotation, secure cookie handling, and explicit logout revocation.

Scope

In scope:

  • Authentik code flow callback handling
  • Authentik token verification via JWKS
  • Internal JWT minting and validation
  • Refresh token persistence and rotation
  • Logout revocation and cookie clearing
  • Cookie policy split for dev vs production

Out of scope:

  • RBAC policy engine
  • Multi-device session management UI
  • Social providers beyond existing Authentik setup

Chosen Approach

Use full OIDC callback exchange with JWKS verification, then mint an internal short-lived JWT and store opaque refresh tokens server-side.

Why this approach:

  • Keeps trust boundary explicit (no blind trust of exchange payload)
  • Allows immediate refresh-token revocation on logout
  • Decouples internal auth contract from external provider claim shape

Defaults

  • Access token TTL: 15 minutes
  • Refresh token TTL: 7 days
  • Cookie mode:
    • Production: Secure=true, SameSite=strict, httpOnly=true
    • Local development: Secure=false, SameSite=lax, httpOnly=true

Architecture

  1. Frontend calls GET /auth/login.
  2. Backend redirects to Authentik authorize endpoint.
  3. Authentik redirects to backend callback with code.
  4. Backend exchanges code for Authentik tokens.
  5. Backend validates Authentik access token using Authentik JWKS (iss, aud, exp, signature).
  6. Backend maps claims to local user record (create/update by authentik_id).
  7. Backend mints internal access JWT and opaque refresh token.
  8. Backend stores hashed refresh token in database and sets cookies.
  9. Protected endpoints validate internal access JWT.
  10. POST /auth/refresh rotates refresh token and issues new access JWT.
  11. POST /auth/logout revokes refresh token and clears cookies.

Data Model

Add a refresh_tokens table:

  • id: UUID primary key
  • user_id: UUID foreign key -> users.id
  • token_hash: string (hash of opaque refresh token; never store raw token)
  • expires_at: timestamp with timezone
  • created_at: timestamp with timezone
  • revoked_at: timestamp with timezone, nullable
  • user_agent: string, nullable
  • ip_address: string, nullable

Indexes:

  • unique index on token_hash
  • index on user_id
  • index on expires_at

API Endpoints

GET /auth/login

  • Redirects to Authentik authorize URL with state and nonce.

GET /auth/callback

  • Validates state.
  • Exchanges code at Authentik token endpoint.
  • Verifies Authentik access token via JWKS.
  • Upserts local user.
  • Mints internal access JWT + opaque refresh token.
  • Persists hashed refresh token record.
  • Sets access_token and refresh_token cookies.

POST /auth/refresh

  • Reads refresh_token cookie.
  • Hashes and finds matching non-revoked, non-expired DB row.
  • If valid, revokes old row and creates a new row (rotation).
  • Mints new internal access JWT and new opaque refresh token.
  • Sets rotated cookies.

POST /auth/logout

  • Reads refresh cookie if present.
  • Revokes corresponding DB token row.
  • Clears access and refresh cookies.

GET /auth/me

  • Validates internal access JWT.
  • Returns current user payload.

Token Strategy

Internal access JWT

Claims:

  • sub: local user id
  • email
  • name
  • roles (optional, if available)
  • iat, exp

Signing:

  • Use configured backend signing secret/algorithm.

Refresh token

  • Opaque, random, high-entropy value
  • Hashed before persistence
  • Rotated on each refresh
  • Revoked on logout and on detected reuse

Security Rules

  • Never expose token contents to frontend JS (httpOnly cookies only).
  • Validate Authentik token signature and critical claims before minting local JWT.
  • Enforce strict cookie attributes by environment.
  • Log security-relevant events with safe redaction.
  • Return generic auth errors to clients; keep details in server logs.

Error Handling

  • Invalid code exchange -> 401
  • JWKS verification failure -> 401
  • Missing/invalid refresh cookie -> 401
  • Revoked/expired refresh token -> 401
  • Reuse detection (if token already rotated/revoked) -> revoke chain and force login

Error body shape:

  • stable machine-readable code
  • non-sensitive message

Testing Strategy

Unit tests:

  • cookie option builder (dev vs prod)
  • Authentik token verification helper
  • internal JWT mint/verify helpers
  • refresh hash + rotation logic

Integration tests:

  • callback creates/updates user and sets cookies
  • refresh rotates token and invalidates previous token
  • logout revokes token and clears cookies
  • protected endpoint rejects invalid/expired JWT

Quality gates:

  • pytest passes
  • mypy . passes
  • ruff check . passes

Implementation Notes

  • Keep auth logic in focused modules (provider client, jwt service, refresh store, route handlers).
  • Keep DB writes idempotent where feasible (user upsert path).
  • Keep changes scoped to auth-oauth and required schema support.