9.6 KiB
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/credentialsis the shared credential management page for both new and existing users?setup=1query 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
- User visits
/login-> server renderslogin.htmlwith username+password form and WebAuthn button - User submits username+password via HTMX POST to
/login/password - Server looks up user by username, fetches password credential from repo
PasswordService.verify()checks the hash- Success -> set
session["userid"]andsession["username"], return HX-Redirect - Failure -> return error HTML fragment (HTMX swaps into the form)
WebAuthn Login
- User enters username, clicks "Sign in with security key"
- HTMX POST to
/login/webauthn/beginwith username - Server looks up user's WebAuthn credentials from repo, calls
WebAuthnService.begin_authentication() - Server stores state in session, returns options JSON
webauthn.jscallsnavigator.credentials.get()with the options- Browser prompts user for security key
- JS serializes the response, HTMX POST to
/login/webauthn/complete - Server calls
WebAuthnService.complete_authentication()with session state + response - Server updates
sign_counton the credential - Success -> set session, return HX-Redirect
- Failure -> return error fragment
Registration
- User visits
/register/{token} - Server calls
MagicLinkService.validate(token)-- if invalid/expired/used -> error page - Server creates user:
generate_unique_userid(), createUser(userid=..., username=link.username, groups=["users"]), save viaUserRepository.create() - Server calls
MagicLinkService.mark_used(token) - Server sets session (
userid,username) - Redirect to
/manage/credentials?setup=1
Credential Management
Page load (GET /manage/credentials):
- Requires authenticated session
- Fetches user's WebAuthn credentials via
CredentialRepository.get_webauthn_by_user() - Fetches password credential via
CredentialRepository.get_password_by_user() - Renders page showing existing credentials with remove buttons, plus forms to add new ones
- If
?setup=1, shows welcome banner
Add WebAuthn credential (begin/complete):
- HTMX POST to
/manage/credentials/webauthn/beginwith optional device name - Server calls
WebAuthnService.begin_registration()with user's ID, existing credential IDs (to exclude) - Stores state in session, returns options JSON
webauthn.jscallsnavigator.credentials.create(), POSTs result to/complete- Server completes registration, stores
WebAuthnCredentialin repo - Returns updated credential list HTML fragment (HTMX swaps)
Set password:
- HTMX POST to
/manage/credentials/passwordwith new password (+confirmation) - Server calls
PasswordService.hash(), stores/updatesPasswordCredentialin repo - 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:
- Write implementation plan — Break into tasks with TDD steps, file contents, test cases
- Implementation — Use subagent-driven development skill
Brainstorming Status
All design questions answered:
- Standalone auth routes (not coupled to OIDC)
- Starlette SessionMiddleware for sessions
- Jinja2 + HTMX + minimal CSS
- Both WebAuthn and password during registration
- WebAuthn challenge state in server-side session
- Credential management shared, not tied to registration
- Credential management at /manage/credentials
- 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)