porchlight/docs/plans/2026-02-13-auth-routes-design.md
2026-02-13 15:45:18 +01:00

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/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:

  • 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)