trails/openspec/changes/archive/2026-05-08-unify-auth-completion/proposal.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

3.8 KiB

Why

Every successful web authentication today inlines the same three-step orchestration in its route handler: record the accepted Terms version on registration (recordTermsAcceptance), mint the session cookie (createSession), and redirect(returnTo ?? "/", { headers }). Four call sites — passkey register-finish, passkey login-finish, magic-link verify-code, and the magic-link click-through consumer — re-implement the same sequence. A bug in any one of them must be fixed in four places, and a future fifth flow (admin impersonation, recovery, etc.) re-derives the same boilerplate.

This change extracts the orchestration into a single function completeAuth and folds in the small file-organization win that comes with it (a new apps/journal/app/lib/auth/ directory). It does not introduce an AuthMethod interface — see ADR-0005 for why polymorphism here is theoretical with no future adapter.

What Changes

  • New module apps/journal/app/lib/auth/completion.ts exporting completeAuth({ userId, isNewRegistration, termsVersion?, request, returnTo? }) → Promise<Response>. Records terms on registration (only), creates the session, returns a redirect(...) Response with the Set-Cookie header attached.
  • New module apps/journal/app/lib/auth/session.ts carrying the cookie session storage (sessionStorage, createSession, getSessionUser, destroySession) extracted from auth.server.ts. The existing path keeps re-exports for backwards compatibility — caller migration is a follow-up if anything.
  • Caller migration: 4 route handlers (api.auth.register.ts passkey-finish branch, api.auth.login.ts passkey-finish-passkey branch, api.auth.login.ts magic-link verify-code branch, auth.verify.tsx magic-link click-through) replace their inlined orchestration with a single completeAuth(...) call.
  • returnTo sanitization centralizes inside completeAuth (was already same-origin checked in callers; now there's one place).
  • Tests: new completion.test.ts covering register-records-terms, non-register-skips-terms, returnTo default, header attachment, returnTo open-redirect rejection. Existing e2e/auth.test.ts continues to pass without modification — that's the proof the refactor is behaviour-preserving.
  • Negative scope (recorded in ADR-0005): no AuthMethod interface, no adapter pattern. Passkey + magic-link is the entire identity surface; OAuth2/PKCE is session transport, not a third method.

Capabilities

New Capabilities

(none — this change reshapes existing capabilities, no new user-visible behaviour.)

Modified Capabilities

  • authentication-methods: requirement-level addition codifying that every post-verify web flow goes through the single completion chokepoint that records terms (on register), creates the session, and redirects. User-visible behaviour unchanged.

Impact

  • Code: 1 new directory (apps/journal/app/lib/auth/), 2 new files (completion.ts, session.ts), auth.server.ts slims down by ~80 lines (the moved session helpers), 4 route handlers shrink. No DB changes. No schema changes.
  • Tests: 1 new unit test file (completion.test.ts). Existing e2e auth test should pass unmodified.
  • Specs: 1 spec gets a small ADDED requirement (authentication-methods/spec.md).
  • Decision references: docs/adr/0004-centralize-auth-completion.md, docs/adr/0005-no-authmethod-polymorphism.md, CONTEXT.md (Authentication section).
  • Out of scope (separate concerns): the OAuth-code issuance flow at /oauth/authorize (operates on already-authenticated users; only shares the trailing redirect), the magic-link request-token step (emits a token, no session created), the add-passkey post-login ceremony (no session creation). Connected-devices UX is its own spec/change. Session-transport changes (cookie ↔ bearer) are orthogonal and not touched here.