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
|
|
@ -0,0 +1,33 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### 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.ts`. The function SHALL be the sole place where a successful web authentication records the accepted Terms version (only on registration), 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.
|
||||
|
||||
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`. `completeAuth` only **records** terms on new registrations; it does not enforce them.
|
||||
|
||||
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, creates the user row, and calls `completeAuth({ userId, isNewRegistration: true, termsVersion, request, returnTo })`
|
||||
- **AND** `completeAuth` writes `users.terms_version` and `users.terms_accepted_at`, 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, isNewRegistration: false, request, returnTo })`
|
||||
- **AND** `completeAuth` skips the terms write (only registrations record terms), 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, isNewRegistration: false, request, returnTo })`
|
||||
|
||||
#### Scenario: Magic-link click-through verify completes through the chokepoint
|
||||
- **WHEN** a visitor opens `/auth/verify?token=<token>` with a valid, unused, unexpired token
|
||||
- **THEN** the route handler consumes the magic token and calls `completeAuth({ userId, isNewRegistration: false, 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue