trails/openspec/changes/archive/2026-05-08-unify-auth-completion/specs/authentication-methods/spec.md
Ullrich Schäfer e268aeec10
Archive unify-auth-completion + sync spec delta (task 5.1)
Applies the ADDED requirement from
openspec/changes/unify-auth-completion/specs/authentication-methods/
into openspec/specs/authentication-methods/spec.md:

- Single web auth completion chokepoint (completeAuth at
  apps/journal/app/lib/auth/completion.server.ts) with five scenarios
  covering passkey register/login finish, magic-link verify-code,
  magic-link click-through, and returnTo sanitization.

Path in the synced requirement is .server.ts (the post-rename name),
not the .ts the delta originally captured.

Change moved to openspec/changes/archive/2026-05-08-unify-auth-completion/.
15/15 tasks complete.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 03:02:51 +02:00

33 lines
3.1 KiB
Markdown

## ADDED Requirements
### Requirement: Single web auth completion chokepoint
Every successful web authentication flow — passkey register-finish, passkey login-finish, magic-link 6-digit-code verify, magic-link click-through verify — SHALL complete by calling a single `completeAuth` function at `apps/journal/app/lib/auth/completion.ts`. The function SHALL be the sole place where a successful web authentication mints the cookie session and constructs the redirect to `returnTo` (or `/` when absent or rejected).
Per-method identity verification (WebAuthn ceremony, magic-token consumption, 6-digit-code consumption) SHALL run in its own function and produce a `userId` *before* `completeAuth` is invoked. `completeAuth` SHALL NOT know how identity was proved.
Terms recording happens at user creation time inside the per-method registration functions (`finishRegistration` for passkey, `registerWithMagicLink` for magic-link), not inside `completeAuth`. The Terms gate (root-loader redirect for cookie sessions; `requireApiUser` 403 for bearer-token API requests) SHALL remain the enforcement point for stale `terms_version`.
OAuth-code issuance at `/oauth/authorize` SHALL NOT be routed through `completeAuth` — that flow operates on an already-authenticated user and shares only the trailing redirect, not the full sequence.
#### Scenario: Passkey register-finish completes through the chokepoint
- **WHEN** a visitor submits a successful WebAuthn `step: "finish"` registration response
- **THEN** the route handler verifies the credential and creates the user row (with terms recorded) inside `finishRegistration`, then calls `completeAuth({ userId, request, returnTo })`
- **AND** `completeAuth` mints the session cookie and returns a `Response` redirecting to `returnTo` (or `/`)
#### Scenario: Passkey login-finish completes through the chokepoint
- **WHEN** a visitor submits a successful WebAuthn `step: "finish-passkey"` login response
- **THEN** the route handler verifies the credential and calls `completeAuth({ userId, request, returnTo })`
- **AND** `completeAuth` mints the session cookie and returns a `Response` redirecting to `returnTo` (or `/`)
#### Scenario: Magic-link 6-digit-code verify completes through the chokepoint
- **WHEN** a visitor submits a valid 6-digit code via `step: "verify-code"`
- **THEN** the route handler consumes the magic token (marks `used_at`) and calls `completeAuth({ userId, request, returnTo })`
#### Scenario: Magic-link click-through verify completes through the chokepoint
- **WHEN** a visitor opens `/auth/verify?token=<token>` with a valid, unused, unexpired token
- **THEN** the route handler consumes the magic token and calls `completeAuth({ userId, request, returnTo })`
#### Scenario: returnTo is sanitized inside completeAuth
- **WHEN** `completeAuth` is called with a `returnTo` value that is not a same-origin absolute path (e.g. starts with `//`, an absolute URL, or is malformed)
- **THEN** the redirect target falls back to `/` rather than honoring the unsafe value
- **AND** every caller benefits from the same check rather than reimplementing it