# Authentication Services — Design Document > **For Claude:** This document captures design decisions for the authentication service layer. > The implementation plan is at `docs/plans/2026-02-13-auth-services-plan.md`. ## Scope Backend-only authentication services. No HTTP routes or templates in this phase. Three independent services: 1. **PasswordService** — Argon2 hash/verify 2. **WebAuthnService** — FIDO2 registration/authentication via python-fido2 3. **MagicLinkService** — Invite token create/validate ## Design Decisions ### 1. PasswordService (stateless, pure crypto) - Wraps `argon2-cffi` `PasswordHasher` - Two methods: `hash(password) -> str`, `verify(hash, password) -> bool` - `PasswordHasher` injected via constructor for testability - No repository dependency — caller reads/writes credentials - Uses argon2-cffi OWASP-recommended defaults - `verify()` returns `bool` (catches `VerifyMismatchError` internally) ### 2. WebAuthnService (class wrapping Fido2Server) - Constructor takes `rp_id`, `rp_name`, `origin` — creates `Fido2Server` internally - Four methods: `begin_registration`, `complete_registration`, `begin_authentication`, `complete_authentication` - `begin_*` methods return `(options, state)` — caller stores state (e.g., in HTTP session) - `complete_*` methods accept state back from caller - No repository dependency — caller handles credential CRUD - Uses python-fido2 v2.1 API (`Fido2Server`, `PublicKeyCredentialRpEntity`, etc.) ### 3. MagicLinkService (token lifecycle) - Depends on `MagicLinkRepository` (injected) - `create(username, created_by?, note?) -> MagicLink` — generates token via `secrets.token_urlsafe(32)` - `validate(token) -> MagicLink | None` — checks exists, not used, not expired - `mark_used(token) -> bool` — delegates to repo - `cleanup_expired() -> int` — delegates to repo - TTL configurable via constructor (from `Settings.invite_ttl`) ### 4. Testing Strategy - **Password:** Roundtrip hash/verify, invalid password returns False, fast hasher params for tests - **WebAuthn:** Build registration/authentication responses manually using fido2's `create()` factory methods (`AttestedCredentialData.create`, `AuthenticatorData.create`, `CollectedClientData.create`, etc.) with real ES256 keys from `cryptography` library. No external test utility package needed. - **MagicLink:** In-memory SQLite fixtures, test create/validate/expired/used states ### 5. Dependencies All already in `pyproject.toml`: - `fido2>=2.1` (python-fido2) - `argon2-cffi>=25.1` No new dependencies required. ## File Structure ``` src/fastapi_oidc_op/ ├── authn/ │ ├── __init__.py (exists, empty) │ ├── password.py (new) │ └── webauthn.py (new) ├── invite/ │ ├── __init__.py (exists, empty) │ └── service.py (new) tests/ ├── test_authn/ │ ├── __init__.py (new) │ ├── test_password.py (new) │ └── test_webauthn.py (new) ├── test_invite/ │ ├── __init__.py (new) │ └── test_service.py (new) ```