226 lines
9.6 KiB
Markdown
226 lines
9.6 KiB
Markdown
# Authentication Routes — Design & Session State
|
|
|
|
> **For Claude:** This document captures the design decisions for Phase 4: authentication routes.
|
|
> Resume from "Next Steps" section.
|
|
|
|
## Project State (as of 2026-02-13)
|
|
|
|
### Git
|
|
|
|
- **Branch:** `main`
|
|
- **HEAD:** `6d80194 docs: update roadmap to reflect completed auth services`
|
|
- **Working tree:** Clean (untracked: `.idea/`, `README.md`, `docs/`)
|
|
- **86 tests passing**, all quality checks green
|
|
|
|
### What Exists (built in previous phases)
|
|
|
|
| Component | Status |
|
|
|-----------|--------|
|
|
| SQLite repositories (User, Credential, MagicLink) | Complete |
|
|
| PasswordService (argon2 hash/verify) | Complete |
|
|
| WebAuthnService (fido2 registration/authentication) | Complete |
|
|
| MagicLinkService (token create/validate/cleanup) | Complete |
|
|
| FastAPI app factory with lifespan, DI, `/health` | Complete |
|
|
| Models (User, WebAuthnCredential, PasswordCredential, MagicLink) | Complete |
|
|
| Config (Settings with env prefix `OIDC_OP_`) | Complete |
|
|
|
|
### What Does NOT Exist Yet
|
|
|
|
- No routes beyond `/health`
|
|
- No templates
|
|
- No static files (HTMX, WebAuthn JS)
|
|
- No session middleware
|
|
- No CLI module (referenced in pyproject.toml but not created)
|
|
- No OIDC provider integration (Phase 5)
|
|
|
|
---
|
|
|
|
## Design Decisions (confirmed by user)
|
|
|
|
### 1. Standalone Auth Routes First
|
|
|
|
Build login/register routes that work independently, without OIDC context. They authenticate
|
|
the user and create a simple session. Phase 5 wires them into the OIDC authorization flow.
|
|
This allows incremental development and testing.
|
|
|
|
### 2. Session Mechanism: Starlette SessionMiddleware
|
|
|
|
Use Starlette's built-in `SessionMiddleware` with signed cookies. After successful authentication,
|
|
the session stores `{"userid": "<userid>", "username": "<username>"}`. This is a temporary
|
|
mechanism that Phase 5 (OIDC integration) will replace with proper OIDC sessions.
|
|
|
|
WebAuthn challenge state is also stored in the session between begin/complete steps.
|
|
|
|
### 3. Templates: Jinja2 + HTMX + Minimal CSS
|
|
|
|
Server-rendered Jinja2 templates with HTMX for interactive form submissions. A small JavaScript
|
|
helper (`webauthn.js`) wraps the browser's `navigator.credentials` API to bridge HTMX and WebAuthn.
|
|
Minimal CSS (no framework).
|
|
|
|
### 4. Registration Supports Both WebAuthn and Password
|
|
|
|
The registration page offers both WebAuthn and password credential creation. User picks one (or both).
|
|
This matches the "WebAuthn-first" philosophy while providing a password fallback.
|
|
|
|
### 5. Credential Management is Shared
|
|
|
|
Credential add/remove is NOT tied to the registration flow. Instead:
|
|
- `/register/{token}` creates the user account, auto-logs in, redirects to `/manage/credentials?setup=1`
|
|
- `/manage/credentials` is the shared credential management page for both new and existing users
|
|
- `?setup=1` query param shows a "Welcome! Set up your first credential" banner
|
|
|
|
This avoids duplicating credential management UI between registration and self-service.
|
|
|
|
### 6. Credential Management Lives at /manage/credentials
|
|
|
|
Per the design doc, `/manage` is the management app prefix. Self-service credential management
|
|
fits here. In Phase 5, `/manage` routes will authenticate via the OIDC RP flow. For now (Phase 4),
|
|
they just check the simple session.
|
|
|
|
---
|
|
|
|
## Endpoint Structure
|
|
|
|
### Login/Logout (authn/routes.py)
|
|
|
|
| Method | Path | Auth | Purpose |
|
|
|--------|------|------|---------|
|
|
| GET | `/login` | No | Render login page |
|
|
| POST | `/login/webauthn/begin` | No | Start WebAuthn authentication (returns challenge) |
|
|
| POST | `/login/webauthn/complete` | No | Verify WebAuthn assertion, create session |
|
|
| POST | `/login/password` | No | Verify username+password, create session |
|
|
| POST | `/logout` | Yes | Clear session |
|
|
| GET | `/register/{token}` | No | Validate magic link, create user, auto-login, redirect |
|
|
|
|
### Credential Management (manage/routes.py)
|
|
|
|
| Method | Path | Auth | Purpose |
|
|
|--------|------|------|---------|
|
|
| GET | `/manage/credentials` | Yes | Render credential management page |
|
|
| POST | `/manage/credentials/webauthn/begin` | Yes | Start WebAuthn registration |
|
|
| POST | `/manage/credentials/webauthn/complete` | Yes | Complete WebAuthn registration |
|
|
| DELETE | `/manage/credentials/webauthn/{credential_id}` | Yes | Remove WebAuthn credential |
|
|
| POST | `/manage/credentials/password` | Yes | Set or change password |
|
|
| DELETE | `/manage/credentials/password` | Yes | Remove password credential |
|
|
|
|
---
|
|
|
|
## Data Flows
|
|
|
|
### Password Login
|
|
|
|
1. User visits `/login` -> server renders `login.html` with username+password form and WebAuthn button
|
|
2. User submits username+password via HTMX POST to `/login/password`
|
|
3. Server looks up user by username, fetches password credential from repo
|
|
4. `PasswordService.verify()` checks the hash
|
|
5. Success -> set `session["userid"]` and `session["username"]`, return HX-Redirect
|
|
6. Failure -> return error HTML fragment (HTMX swaps into the form)
|
|
|
|
### WebAuthn Login
|
|
|
|
1. User enters username, clicks "Sign in with security key"
|
|
2. HTMX POST to `/login/webauthn/begin` with username
|
|
3. Server looks up user's WebAuthn credentials from repo, calls `WebAuthnService.begin_authentication()`
|
|
4. Server stores state in session, returns options JSON
|
|
5. `webauthn.js` calls `navigator.credentials.get()` with the options
|
|
6. Browser prompts user for security key
|
|
7. JS serializes the response, HTMX POST to `/login/webauthn/complete`
|
|
8. Server calls `WebAuthnService.complete_authentication()` with session state + response
|
|
9. Server updates `sign_count` on the credential
|
|
10. Success -> set session, return HX-Redirect
|
|
11. Failure -> return error fragment
|
|
|
|
### Registration
|
|
|
|
1. User visits `/register/{token}`
|
|
2. Server calls `MagicLinkService.validate(token)` -- if invalid/expired/used -> error page
|
|
3. Server creates user: `generate_unique_userid()`, create `User(userid=..., username=link.username, groups=["users"])`, save via `UserRepository.create()`
|
|
4. Server calls `MagicLinkService.mark_used(token)`
|
|
5. Server sets session (`userid`, `username`)
|
|
6. Redirect to `/manage/credentials?setup=1`
|
|
|
|
### Credential Management
|
|
|
|
**Page load (`GET /manage/credentials`):**
|
|
1. Requires authenticated session
|
|
2. Fetches user's WebAuthn credentials via `CredentialRepository.get_webauthn_by_user()`
|
|
3. Fetches password credential via `CredentialRepository.get_password_by_user()`
|
|
4. Renders page showing existing credentials with remove buttons, plus forms to add new ones
|
|
5. If `?setup=1`, shows welcome banner
|
|
|
|
**Add WebAuthn credential (begin/complete):**
|
|
1. HTMX POST to `/manage/credentials/webauthn/begin` with optional device name
|
|
2. Server calls `WebAuthnService.begin_registration()` with user's ID, existing credential IDs (to exclude)
|
|
3. Stores state in session, returns options JSON
|
|
4. `webauthn.js` calls `navigator.credentials.create()`, POSTs result to `/complete`
|
|
5. Server completes registration, stores `WebAuthnCredential` in repo
|
|
6. Returns updated credential list HTML fragment (HTMX swaps)
|
|
|
|
**Set password:**
|
|
1. HTMX POST to `/manage/credentials/password` with new password (+confirmation)
|
|
2. Server calls `PasswordService.hash()`, stores/updates `PasswordCredential` in repo
|
|
3. Returns updated credential section fragment
|
|
|
|
**Remove credentials:**
|
|
- DELETE requests via HTMX, server deletes from repo, returns updated list fragment
|
|
- Guard against removing the last credential (user must keep at least one)
|
|
|
|
---
|
|
|
|
## Files to Create/Modify
|
|
|
|
| File | Action | Purpose |
|
|
|------|--------|---------|
|
|
| `src/fastapi_oidc_op/authn/routes.py` | Create | Login/logout/register routes |
|
|
| `src/fastapi_oidc_op/manage/routes.py` | Create | Credential management routes |
|
|
| `src/fastapi_oidc_op/templates/base.html` | Create | Base template (HTMX, minimal CSS) |
|
|
| `src/fastapi_oidc_op/templates/login.html` | Create | Login page |
|
|
| `src/fastapi_oidc_op/templates/manage/credentials.html` | Create | Credential management page |
|
|
| `src/fastapi_oidc_op/static/webauthn.js` | Create | Browser WebAuthn helper |
|
|
| `src/fastapi_oidc_op/static/htmx.min.js` | Create | HTMX library (vendored) |
|
|
| `src/fastapi_oidc_op/static/style.css` | Create | Minimal CSS |
|
|
| `src/fastapi_oidc_op/app.py` | Modify | Mount routers, add SessionMiddleware, configure templates/static |
|
|
| `src/fastapi_oidc_op/dependencies.py` | Modify | Add session/auth dependencies |
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
- Invalid/expired/used magic link -> render error page (not redirect, to avoid loops)
|
|
- Wrong password -> HTMX error fragment swapped into form
|
|
- WebAuthn failure -> HTMX error fragment
|
|
- Not authenticated -> redirect to `/login`
|
|
- Attempt to remove last credential -> HTMX error fragment with explanation
|
|
- User not found during login -> same error as wrong password (no username enumeration)
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
Design is validated through Section 4 (registration & credential management flows).
|
|
Remaining before implementation:
|
|
|
|
1. **Write implementation plan** — Break into tasks with TDD steps, file contents, test cases
|
|
2. **Implementation** — Use subagent-driven development skill
|
|
|
|
### Brainstorming Status
|
|
|
|
All design questions answered:
|
|
- [x] Standalone auth routes (not coupled to OIDC)
|
|
- [x] Starlette SessionMiddleware for sessions
|
|
- [x] Jinja2 + HTMX + minimal CSS
|
|
- [x] Both WebAuthn and password during registration
|
|
- [x] WebAuthn challenge state in server-side session
|
|
- [x] Credential management shared, not tied to registration
|
|
- [x] Credential management at /manage/credentials
|
|
- [x] Registration redirects to /manage/credentials?setup=1
|
|
|
|
---
|
|
|
|
## Process Notes
|
|
|
|
- User prefers to be asked design questions before implementation begins
|
|
- Use **brainstorming** skill: one question at a time, present design in 200-300 word sections, validate each
|
|
- Use **subagent-driven development** for implementation
|
|
- Plans live in `docs/plans/`
|
|
- Quality gate: `./scripts/check.sh` (ruff format, ruff check, ty check, pytest)
|