From e268aeec10acb2fb6343edd7042701aab9b6634f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 8 May 2026 03:02:51 +0200 Subject: [PATCH] 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) --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/authentication-methods/spec.md | 0 .../tasks.md | 2 +- openspec/specs/authentication-methods/spec.md | 32 +++++++++++++++++++ 6 files changed, 33 insertions(+), 1 deletion(-) rename openspec/changes/{unify-auth-completion => archive/2026-05-08-unify-auth-completion}/.openspec.yaml (100%) rename openspec/changes/{unify-auth-completion => archive/2026-05-08-unify-auth-completion}/design.md (100%) rename openspec/changes/{unify-auth-completion => archive/2026-05-08-unify-auth-completion}/proposal.md (100%) rename openspec/changes/{unify-auth-completion => archive/2026-05-08-unify-auth-completion}/specs/authentication-methods/spec.md (100%) rename openspec/changes/{unify-auth-completion => archive/2026-05-08-unify-auth-completion}/tasks.md (97%) diff --git a/openspec/changes/unify-auth-completion/.openspec.yaml b/openspec/changes/archive/2026-05-08-unify-auth-completion/.openspec.yaml similarity index 100% rename from openspec/changes/unify-auth-completion/.openspec.yaml rename to openspec/changes/archive/2026-05-08-unify-auth-completion/.openspec.yaml diff --git a/openspec/changes/unify-auth-completion/design.md b/openspec/changes/archive/2026-05-08-unify-auth-completion/design.md similarity index 100% rename from openspec/changes/unify-auth-completion/design.md rename to openspec/changes/archive/2026-05-08-unify-auth-completion/design.md diff --git a/openspec/changes/unify-auth-completion/proposal.md b/openspec/changes/archive/2026-05-08-unify-auth-completion/proposal.md similarity index 100% rename from openspec/changes/unify-auth-completion/proposal.md rename to openspec/changes/archive/2026-05-08-unify-auth-completion/proposal.md diff --git a/openspec/changes/unify-auth-completion/specs/authentication-methods/spec.md b/openspec/changes/archive/2026-05-08-unify-auth-completion/specs/authentication-methods/spec.md similarity index 100% rename from openspec/changes/unify-auth-completion/specs/authentication-methods/spec.md rename to openspec/changes/archive/2026-05-08-unify-auth-completion/specs/authentication-methods/spec.md diff --git a/openspec/changes/unify-auth-completion/tasks.md b/openspec/changes/archive/2026-05-08-unify-auth-completion/tasks.md similarity index 97% rename from openspec/changes/unify-auth-completion/tasks.md rename to openspec/changes/archive/2026-05-08-unify-auth-completion/tasks.md index 0bdceff..e2dfba7 100644 --- a/openspec/changes/unify-auth-completion/tasks.md +++ b/openspec/changes/archive/2026-05-08-unify-auth-completion/tasks.md @@ -26,5 +26,5 @@ ## 5. Documentation + follow-up -- [ ] 5.1 At archive time, apply the spec delta in `specs/authentication-methods/` to `openspec/specs/`. +- [x] 5.1 At archive time, apply the spec delta in `specs/authentication-methods/` to `openspec/specs/`. - [x] 5.2 Update import paths app-wide from `auth.server.ts` to `./auth/session.ts` and drop the re-exports. (Folded into this PR after the smoke test confirmed everything works.) diff --git a/openspec/specs/authentication-methods/spec.md b/openspec/specs/authentication-methods/spec.md index 64e8995..9fb5c5b 100644 --- a/openspec/specs/authentication-methods/spec.md +++ b/openspec/specs/authentication-methods/spec.md @@ -93,3 +93,35 @@ A signed-in user SHALL be able to remove a passkey from their account via the Se #### Scenario: Last-passkey safety net (verified email is the fallback) - **WHEN** a user attempts to delete their only remaining passkey - **THEN** the action proceeds because magic-link login by email is always available — passkey deletion does not lock the user out + +### 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.server.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=` 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