# 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": "", "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)