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>
2.9 KiB
2.9 KiB
1. New auth module structure
- 1.1 Create
apps/journal/app/lib/auth/directory. - 1.2 Create
apps/journal/app/lib/auth/session.tsand movesessionStorage,createSession,getSessionUser,destroySessionfromauth.server.ts(preserve behaviour, includingprocess.env.SESSION_SECRETsource). - 1.3 In
auth.server.ts, re-export the moved helpers from./auth/session.tsso existing imports keep working unchanged. Add a JSDoc@deprecated-style comment pointing at the new path.
2. completeAuth chokepoint
- 2.1 Write
apps/journal/app/lib/auth/completion.test.ts(TDD red): scenarios for new-registration writes terms, non-registration skips terms, returnTo defaults to/, returnTo//evil.comrejected, returnTohttps://evil.comrejected, response includes Set-Cookie. - 2.2 Create
apps/journal/app/lib/auth/completion.tsexportingcompleteAuth({ userId, isNewRegistration, termsVersion?, request, returnTo? }) → Promise<Response>and a privatesafeReturnTo(value)helper. Implementation: asserttermsVersionwhenisNewRegistration; ifisNewRegistration,recordTermsAcceptance(userId, termsVersion);createSession(userId, request);redirect(safeReturnTo(returnTo) ?? "/", { headers }). - 2.3 Run completion tests green.
3. Caller migration
- 3.1
apps/journal/app/routes/api.auth.register.tspasskey-finish branch — replace inlined session+redirect withreturn completeAuth({ userId, isNewRegistration: true, termsVersion, request, returnTo }). Drop now-unused imports. - 3.2
apps/journal/app/routes/api.auth.login.tspasskeystep: "finish-passkey"branch — replace withreturn completeAuth({ userId, isNewRegistration: false, request, returnTo }). - 3.3
apps/journal/app/routes/api.auth.login.tsmagic-linkstep: "verify-code"branch — replace withreturn completeAuth(...). - 3.4
apps/journal/app/routes/auth.verify.tsxmagic-link click-through consumer — replace withreturn completeAuth(...). - 3.5 Confirm no other callers of
createSessionremain inside auth route handlers (they should all flow throughcompleteAuth).getSessionUseranddestroySessioncontinue to be called directly from non-completion sites — that's expected.
4. Verification
- 4.1
pnpm typecheck && pnpm lint && pnpm testgreen. - 4.2
pnpm test:e2e(auth flows) green without modification — proves behaviour-preserving refactor. - 4.3 Manual sanity: register with passkey locally, login with passkey, log out, re-login via magic-link 6-digit code, click-through magic link from
auth.verify.tsx. Confirm session cookie set + correct redirect each time.
5. Documentation + follow-up
- 5.1 At archive time, apply the spec delta in
specs/authentication-methods/toopenspec/specs/. - 5.2 (Optional follow-up — not part of this change) update import paths app-wide from
auth.server.tsto./auth/session.tsand drop the re-exports. Track separately.