Extract the post-verify orchestration shared across passkey register-finish, passkey login-finish, magic-link verify-code, and magic-link click-through into a single completeAuth function. Two ADRs record the decision: - ADR-0004: centralize web auth completion (record terms + create session + redirect) in apps/journal/app/lib/auth/completion.ts. - ADR-0005: explicitly no AuthMethod polymorphism. Passkey + magic- link is the entire identity surface; OAuth2/PKCE is session transport, not a peer method. Recorded as a negative decision so future architecture passes don't re-suggest extracting the interface. CONTEXT.md gains an Authentication section covering completeAuth, the two methods, the OAuth2-as-transport distinction, and where the Terms gate enforcement lives (root loader for web, requireApiUser for API per the just-merged mobile-terms-gate). OpenSpec change unify-auth-completion captures the proposal, design (5 decisions including the negative-scope choices), spec delta on authentication-methods, and 14 tasks. Implementation follows on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.tsexportingcompleteAuth({ userId, isNewRegistration, termsVersion?, request, returnTo? }) → Promise<Response>. Records terms on registration (only), creates the session, returns aredirect(...)Response with theSet-Cookieheader attached. - New module
apps/journal/app/lib/auth/session.tscarrying the cookie session storage (sessionStorage,createSession,getSessionUser,destroySession) extracted fromauth.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.tspasskey-finish branch,api.auth.login.tspasskey-finish-passkey branch,api.auth.login.tsmagic-link verify-code branch,auth.verify.tsxmagic-link click-through) replace their inlined orchestration with a singlecompleteAuth(...)call. returnTosanitization centralizes insidecompleteAuth(was already same-origin checked in callers; now there's one place).- Tests: new
completion.test.tscovering register-records-terms, non-register-skips-terms, returnTo default, header attachment, returnTo open-redirect rejection. Existinge2e/auth.test.tscontinues to pass without modification — that's the proof the refactor is behaviour-preserving. - Negative scope (recorded in ADR-0005): no
AuthMethodinterface, 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.tsslims 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.