4.9 KiB
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
- Production:
Architecture
- Frontend calls
GET /auth/login. - Backend redirects to Authentik authorize endpoint.
- Authentik redirects to backend callback with
code. - Backend exchanges
codefor Authentik tokens. - Backend validates Authentik access token using Authentik JWKS (
iss,aud,exp, signature). - Backend maps claims to local user record (create/update by
authentik_id). - Backend mints internal access JWT and opaque refresh token.
- Backend stores hashed refresh token in database and sets cookies.
- Protected endpoints validate internal access JWT.
POST /auth/refreshrotates refresh token and issues new access JWT.POST /auth/logoutrevokes refresh token and clears cookies.
Data Model
Add a refresh_tokens table:
id: UUID primary keyuser_id: UUID foreign key ->users.idtoken_hash: string (hash of opaque refresh token; never store raw token)expires_at: timestamp with timezonecreated_at: timestamp with timezonerevoked_at: timestamp with timezone, nullableuser_agent: string, nullableip_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_tokenandrefresh_tokencookies.
POST /auth/refresh
- Reads
refresh_tokencookie. - 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 idemailnameroles(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:
pytestpassesmypy .passesruff 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.