171 lines
4.9 KiB
Markdown
171 lines
4.9 KiB
Markdown
# 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.
|