Add unify-auth-completion architecture artifacts
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>
This commit is contained in:
parent
1a0a212139
commit
7e22f1260b
8 changed files with 320 additions and 0 deletions
26
docs/adr/0004-centralize-auth-completion.md
Normal file
26
docs/adr/0004-centralize-auth-completion.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# Centralize web auth completion in `completeAuth`
|
||||
|
||||
Every successful web authentication today (passkey login, passkey
|
||||
register, magic-link code verify, magic-link click-through verify)
|
||||
inlines the same three-step orchestration in its route handler:
|
||||
record the accepted Terms version on registration, mint the session
|
||||
cookie, and redirect to `returnTo` (or `/`). With four call sites
|
||||
re-implementing the same sequence, a bug in any of them — a missed
|
||||
terms write, a wrong cookie option, an open-redirect-prone returnTo
|
||||
— must be fixed in four places, and a future fifth flow (e.g. an
|
||||
admin-impersonation login, a one-time recovery flow) re-derives the
|
||||
same boilerplate.
|
||||
|
||||
We extract the orchestration into a single function
|
||||
`completeAuth({ userId, isNewRegistration, termsVersion?, request,
|
||||
returnTo? })` at `apps/journal/app/lib/auth/completion.ts`. Per-method
|
||||
identity verification (WebAuthn ceremony, magic-token consumption)
|
||||
stays in its own function and runs *before* `completeAuth` —
|
||||
the chokepoint deliberately knows nothing about how identity was
|
||||
proved. Terms enforcement (the gate) remains middleware (root loader
|
||||
for web, `requireApiUser` for API); `completeAuth` only *records*
|
||||
terms on registration. The OAuth-code-issuance flow at
|
||||
`/oauth/authorize` is **not** routed through `completeAuth` — it
|
||||
operates on an already-authenticated user and shares only the
|
||||
trailing redirect, not the full sequence (see ADR-0005 for the
|
||||
broader scope decision).
|
||||
40
docs/adr/0005-no-authmethod-polymorphism.md
Normal file
40
docs/adr/0005-no-authmethod-polymorphism.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# No `AuthMethod` polymorphism — passkey + magic-link is the entire surface
|
||||
|
||||
A natural temptation when reshaping auth is to extract an
|
||||
`AuthMethod` interface (analogous to the connected-services
|
||||
`Importer` / `RoutePusher` / `WebhookReceiver` capability seams) so
|
||||
passkey and magic-link become two adapters of a common shape, with
|
||||
future Google/Apple/SAML/etc. as additional adapters. We are
|
||||
explicitly **not** doing that, and recording the negative decision
|
||||
here so future architecture passes don't re-suggest it.
|
||||
|
||||
Two reasons. First, the *number of adapters is fixed*: passkey is
|
||||
trails.cool's preferred identity method and magic-link is its
|
||||
fallback for browsers without WebAuthn support and cross-device sign-
|
||||
in. There are no plans for additional methods. Social sign-in (Google,
|
||||
Apple) would conflict with the privacy-first ethos in `CLAUDE.md`
|
||||
(third-party identity providers see every login), would fight the
|
||||
ActivityPub federation model (centralized OIDC vs.
|
||||
`user@domain` identity), and would add account-linking edge cases
|
||||
without a UX win — passkeys already deliver the one-tap convenience
|
||||
that justifies social sign-in elsewhere. With two adapters and no
|
||||
forecast third, an `AuthMethod` interface is theoretical
|
||||
polymorphism with no future second customer; the deletion test fails.
|
||||
|
||||
Second, the *interface shape would be shallow*. WebAuthn is a
|
||||
multi-step ceremony (start → finish, with challenge persistence
|
||||
between calls); magic-link is a request-then-verify flow with a
|
||||
15-minute token in the database. Forcing both into a unified
|
||||
`startAuth(input) → finishAuth(response)` interface produces an
|
||||
adapter where each implementation barely shares a type signature —
|
||||
the two flows have genuinely different shapes, and the seam would
|
||||
fill with optional methods and discriminated unions. The real
|
||||
repetition lives in the *post-verify orchestration* (record terms,
|
||||
mint session, redirect), which ADR-0004 addresses with a single
|
||||
`completeAuth` function — no polymorphism required.
|
||||
|
||||
OAuth2/PKCE in the `mobile-app` change is sometimes mistaken for a
|
||||
third auth method. It isn't: mobile users still authenticate via
|
||||
passkey or magic-link in a WebView, and OAuth2 only exchanges the
|
||||
resulting code for long-lived bearer tokens. OAuth2 is **session
|
||||
transport** (peer of: HTTP-only cookie), not identity proof.
|
||||
Loading…
Add table
Add a link
Reference in a new issue