# Authentication Routes Implementation Plan (Phase 4) > **Status: COMPLETE** — All 10 tasks implemented and passing. 120 tests, full quality gate green. > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add standalone login/registration + credential management routes (no OIDC yet) using sessions, Jinja2 templates, HTMX, and the existing auth services + repositories. **Architecture:** FastAPI routers for `authn` and `manage` are mounted in `create_app()`. Starlette `SessionMiddleware` stores a minimal session (`userid`, `username`) plus WebAuthn transient state. HTML is server-rendered with Jinja2; HTMX progressively enhances forms; `webauthn.js` bridges browser WebAuthn APIs. Auth services (`PasswordService`, `WebAuthnService`, `MagicLinkService`) are instantiated in the lifespan and stored on `app.state` alongside the repositories. **Tech Stack:** FastAPI, Starlette SessionMiddleware, Jinja2 templates, HTMX (vendored), python-fido2 >=2.1, argon2-cffi, aiosqlite. **Quality gate:** `./scripts/check.sh` **Known constraints:** - Starlette `SessionMiddleware` uses signed cookies (~4KB limit). WebAuthn challenge state is small (~100 bytes), so this is fine. If state grows, switch to server-side session storage. - The `fido2` library's `Fido2Server.authenticate_complete()` returns `AttestedCredentialData` (matched credential), NOT the new sign count. The sign count must be extracted from the raw `AuthenticationResponse.response.authenticator_data.counter`. - WebAuthn options returned by `fido2` contain `bytes` fields (`challenge`, `user.id`, credential IDs). The library provides `fido2.utils.websafe_encode`/`websafe_decode` for base64url conversion. The `dict(options)` output from `begin_registration`/`begin_authentication` is already JSON-serializable as the library handles encoding internally via its CBOR/JSON mapping. **Discoveries during implementation:** - `itsdangerous` package was needed for Starlette's `SessionMiddleware` — added via `uv add itsdangerous` - `ty` type checker flags `app.add_middleware(SessionMiddleware, ...)` as invalid argument type — needs `# type: ignore[arg-type]` - The `_count_credentials` helper in `manage/routes.py` needs `# type: ignore[union-attr]` on the cred_repo calls since it takes `object` type - Magic link service is at `fastapi_oidc_op.invite.service.MagicLinkService` (not `authn/magic_link.py`) - The `PasswordService.verify()` takes `(password_hash, password)` — hash first, then plaintext - `AttestedCredentialData` is a `bytes` subclass — reconstruct from stored bytes via `AttestedCredentialData(stored_bytes)`, not `from_ctap_object()` --- ### Task 1: Config + App Wiring + Templates + Static Files [DONE] **Files:** - Modify: `src/fastapi_oidc_op/config.py` - Modify: `src/fastapi_oidc_op/app.py` - Create: `src/fastapi_oidc_op/authn/routes.py` - Create: `src/fastapi_oidc_op/manage/routes.py` - Create: `src/fastapi_oidc_op/templates/base.html` - Create: `src/fastapi_oidc_op/templates/login.html` - Create: `src/fastapi_oidc_op/static/style.css` - Create: `src/fastapi_oidc_op/static/htmx.min.js` - Create: `tests/test_auth_routes/__init__.py` - Create: `tests/test_auth_routes/test_pages.py` **Why merged:** The original plan had Tasks 1 and 2 as separate steps, but Task 1's tests could never pass without Task 2's templates and static files. Merging them gives a clean red-green cycle. **Step 1: Write the failing tests** Create `tests/test_auth_routes/__init__.py` (empty file). Create `tests/test_auth_routes/test_pages.py`: ```python from httpx import AsyncClient async def test_get_login_page_contains_form(client: AsyncClient) -> None: res = await client.get("/login") assert res.status_code == 200 assert "