PR 1910 closes a customer login CSRF and session-fixation path. A magic link now carries a public initiation selector while the requesting browser retains an independent 128-bit nonce. The API stores only the nonce hash. A matching browser can redeem silently; any other browser receives masked account context and must explicitly continue before the single-use token is claimed. Session-authority guards prevent a delayed confirmation from replacing a valid session adopted from another tab.
54b6b4219 (merge-base with main)…b046e9fde (captured implementation head)
Source state
fix/1902-magic-link-confirmation at b046e9fde, pushed as PR #1910; this report is the following evidence-only commit
Generated
Reviewer brief
Outcome and scope
Forwarded links require a deliberate account adoption
The browser that requested a link proves continuity with a nonce that never enters the emailed URL. A forwarded or legacy link remains usable, but only after the visitor sees masked identity context and activates Continue. The bearer is stripped from the fragment before the request and is held in a React ref rather than rendered state. Existing valid sessions and later cross-tab session adoption remain authoritative.
What this report proves
The screenshot uses the production widget bundle built from implementation head 25306e986, whose widget source is unchanged at b046e9fde, in a desktop viewport with a mocked 409 preview response. Browser inspection observed the bearer removed from the URL and absent from both light DOM and shadow DOM. Focused API, database, widget, migration and real Chromium checks cover the security state transitions. The report does not prove production deployment order or replace CI and pull request review.
Claim map
Evidence coverage
Each material changed behavior or state maps to direct evidence or an explicit gap.
Behavior or claim
Evidence
Status
A link without matching browser initiation state does not silently create a session
API 409 enforcement, confirmation screenshot, widget unit tests and real Chromium forwarded-link test
Covered
The requesting browser can redeem silently with its stored nonce
Database-backed nonce-hash assertion plus real Chromium same-browser test
Covered
The visitor sees enough non-sensitive identity context to recognize the account
Masked-email API payload and rendered confirmation screenshot
Covered
Existing and newly adopted valid sessions take precedence over an inbound link
Generation and authority guards plus deferred-response tests for both race orderings
Covered
Tenant binding, expiry and single-use redemption remain enforced
Brand-scoped candidate query and atomic claim excerpt, API unit coverage and live PostgreSQL replay rejection
Covered
The browser regression test executes through the default repository CI command
widgets-test package script and browser runner, exercised locally through bun run test
Covered
Observed change
Review evidence
Evidence is grouped by the reviewer question each item addresses.
Forwarded-link interaction
Screenshot
The signed-out visitor must choose whether to adopt the account
The real built account widget received the same masked preview shape returned by the API. The bearer had already been removed from the address and did not appear in the document or widget shadow DOM. The dialog exposes only a masked email and offers Cancel and Continue.
Head at 25306e986: explicit confirmation before cross-device session adoption.After
Authentication state transitions
Table
Browser continuity determines whether user intent is required
The raw bearer remains the same single-use credential. Browser continuity changes only the gate before the atomic claim.
Scenario
Server result
Widget behavior
Matching initiation ID and nonce
200 after atomic token claim
Session is adopted without an extra prompt
Missing, stale or mismatched nonce
409 with masked email; token remains unused
Dialog requires Continue before a confirm request
Valid stored customer or impersonation session
Inbound token is not submitted
Current session remains authoritative
Valid sibling session arrives during confirmation
Late confirmation response is ignored
Newly adopted session remains authoritative
Junk sibling token fails validation
Successful confirmation remains usable
Failed candidate cannot discard the minted session
API confirmation boundary
Code
A mismatched browser receives preview data without consuming the bearer
The token candidate is tenant-scoped, unused and unexpired. Without a hash match or explicit confirmation, the route returns only a masked email and does not enter the claiming transaction.
Confirmation gate at implementation head b046e9fde.After
Confirmation still enters the existing single-use claim
After the gate, the route locks the tenant-scoped customer and marks the selected token used only when it is still unused and unexpired. Concurrent requests cannot both return a claimed row.
Atomic claim at implementation head b046e9fde.After
A delayed confirmation cannot overwrite newer session authority
The bearer remains in a ref. Continue captures the current authority and applies its response only while the same provider, authority and pending token remain active. Successful session adoption invalidates those conditions.
Confirmation commit guard at implementation head b046e9fde.After
Migration 0191 adds one nullable fixed-length hash column. The raw browser nonce remains client-side and existing rows flow through explicit confirmation in the updated widget.
ALTER TABLE "magic_link_tokens" ADD COLUMN "browser_nonce_hash" varchar(64);
Permanent browser regression
Code
The default widgets-test command builds and exercises Chromium
The runner builds the current bundle and harness, ensures Chromium is available, starts a strict-port Vite preview, waits for readiness, and runs the forwarded and same-browser cases with browser tests enabled.
Browser runner at implementation head b046e9fde.After
packages/widgets-test/run-browser-tests.ts:20-78
if ((await run(["bun", "run", "build"])) !== 0) process.exit(1);
if (!existsSync(chromium.executablePath())) {
if ((await run(["bunx", "playwright", "install", "chromium"])) !== 0) {
process.exit(1);
}
}
const preview = Bun.spawn([
"bunx", "vite", "preview", "--host", host,
"--port", String(port), "--strictPort",
], { cwd, stdin: "ignore", stdout: "inherit", stderr: "inherit" });
// Wait for account.html, then run the real browser suite.
const exitCode = await run(
["bun", "test", "magic-link-confirmation.browser.test.ts"],
{ RUN_BROWSER_TESTS: "1", WIDGETS_TEST_BASE_URL: baseUrl },
);
Confidence
Validation
Pass
API authentication behavior
36 tests passed with 115 assertions in widget-auth.test.ts, including masked preview, nonce match, explicit confirmation, replay rejection and email-verification effects
Pass
Database-backed customer authentication
4 magic-link capture tests passed with 30 assertions, and 31 session-revocation tests passed with 65 assertions against isolated PostgreSQL. Coverage includes stored nonce hash, same-browser redemption, cross-device confirmation, replay rejection and revocation interleavings.
Pass
Widget authentication and checkout behavior
37 tests passed with 199 assertions across auth.test.tsx and GiftCardPurchase.test.tsx, including both deferred session-authority race orderings
Pass
Real browser fixation coverage
The default widgets-test command passed 5 harness tests and 2 Chromium tests. The forwarded link required Continue and the requesting browser redeemed silently with its nonce.
Pass
Migration chain and schema convergence
191 migrations applied to an empty PostgreSQL database, a second migrate was idempotent, and the resulting 96-table, 56-enum schema matched drizzle-kit push
Pass
Type and style checks
API and widgets TypeScript checks exited 0. Biome reported no diagnostics across all changed source, test and package files. The 10-test credential-classification suite passed after the nonce hash was added to the dev-panel redaction policy.
Pass
Independent review
Independent code and security reviewers found and drove fixes for session-adoption races and skipped browser coverage. Focused final reviews reported no remaining material issue.
Limits
Known issues, risks, and gaps
Deployment coordination and legacy-client behavior are explicit
Widget and API deployment order matters A cached old widget does not understand the API's 409 confirmation response. Deploy the updated widget before or with the API. New widgets remain compatible with old API responses during the rollout, while enforcing confirmation once the API is current.
Existing unbound rows require confirmation Rows created before migration 0191 have no nonce hash. The updated widget treats them like cross-device links and keeps them usable through the explicit Continue action rather than silently redeeming them.
The UI capture uses a mocked API response The screenshot proves rendering, masked context, fragment removal and DOM secrecy in the production widget bundle. API enforcement and database mutation behavior are proven separately by focused unit and live PostgreSQL tests.