Skip to content

Passkey welcome back#674

Merged
kentcdodds merged 21 commits intomainfrom
cursor/passkey-welcome-back-1d0f
Feb 24, 2026
Merged

Passkey welcome back#674
kentcdodds merged 21 commits intomainfrom
cursor/passkey-welcome-back-1d0f

Conversation

@kentcdodds
Copy link
Owner

@kentcdodds kentcdodds commented Feb 23, 2026

Implement passkey form autofill ('welcome back') on the login page for a privacy-friendly and seamless sign-in experience.


Open in Web Open in Cursor 


Note

Medium Risk
Changes WebAuthn authentication flow and server-side session/cookie behavior, which can affect login reliability across browsers. The scope is contained and backed by an end-to-end test, but regressions could block sign-in.

Overview
Adds passkey form-autofill (WebAuthn conditional UI) to the /login page by prefetching authentication options, starting startAuthentication with useBrowserAutofill, and cancelling pending ceremonies on unmount or when switching to explicit passkey login.

Refactors client verification into verifyPasskeyWithServer, preserves server-sent WebAuthn request option fields, updates the email field autocomplete to username webauthn, and shows a tip when autofill is supported.

On successful passkey verification, the server now also clears the traditional login-info session, getWebAuthnConfig now requires discoverable credentials (residentKey: 'required'), and a new Playwright E2E test covers passkey registration plus autofill-based sign-in.

Written by Cursor Bugbot for commit b3d9e92. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Passkey autofill on the login page with a UI hint to use the browser's form autofill for faster sign-in
    • Updated welcome message after login ("Welcome back! Navigating to your account page.")
  • Improvements

    • Enforced discoverable (resident) passkeys for privacy-friendly, autofill-capable sign-in
    • More reliable passkey flow that safely cancels ongoing authentication when leaving the page
    • Email input optimized for passkey autofill compatibility
    • Autofill flow resets safely on failure
  • Tests

    • Added end-to-end tests covering passkey registration and autofill-based login workflows

@cursor
Copy link

cursor bot commented Feb 23, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Detects browser WebAuthn autofill on the login page, orchestrates conditional passkey authentication (start/cancel/cleanup), requires resident credentials server-side, clears legacy login session data during verify, updates UI/autocomplete and success messaging, and adds an E2E Playwright test for passkey autofill sign-in.

Changes

Cohort / File(s) Summary
Login Flow & Autofill UI
app/routes/login.tsx
Adds WebAuthn autofill detection and lifecycle (init, start, cancel, cleanup), passkeyAutofill state and reset key, updates status messages and final success text, renders an autofill tip, and changes email autocomplete to username webauthn.
WebAuthn Verification & Session Management
app/routes/resources/webauthn/verify-authentication.ts
Merges headers from delete-passkey cookie, WebAuthn session, and login info session; invokes getLoginInfoSession().clean() to clear legacy email/error before returning success response.
WebAuthn Server Config
app/utils/webauthn.server.ts
Changes authenticatorSelection.residentKey from 'preferred' to 'required' to enforce discoverable (resident) credentials for form autofill / conditional UI sign-in.
End-to-End Testing
e2e/passkey-form-autofill.spec.ts
Adds Playwright E2E test that registers a passkey with a virtual authenticator (ensures resident credential), verifies resident-key requirement, asserts email autocomplete, simulates conditional UI/autofill login with fallback, and validates navigation to profile.
Imports & Minor Edits
app/routes/login.tsx, app/routes/resources/webauthn/verify-authentication.ts
Minor import reordering/grouping and small formatting adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant LoginPage as Login Page
    participant WebAuthnAPI as WebAuthn API
    participant Server
    participant LoginSession as Login Session

    Browser->>LoginPage: Load login page
    LoginPage->>WebAuthnAPI: Check autofill support
    WebAuthnAPI-->>LoginPage: Autofill supported
    LoginPage->>Server: Request authentication options (residentKey required)
    Server-->>LoginPage: Return options
    LoginPage->>WebAuthnAPI: Start conditional authentication (autofill)
    WebAuthnAPI-->>Browser: Show conditional UI / autofill prompt
    Browser->>WebAuthnAPI: User selects passkey → assertion
    WebAuthnAPI-->>LoginPage: Assertion returned
    LoginPage->>Server: Submit assertion for verification
    Server->>LoginSession: Clear legacy login info (clean)
    LoginSession-->>Server: Confirm cleaned
    Server-->>LoginPage: Verification success + merged headers
    LoginPage->>Browser: Navigate to user account
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I nibble at keys both new and old,

A gentle prompt — a passkey bold.
Resident secrets snug and warm,
Autofill guides me through the storm,
Welcome back — I hop to your account with a charm. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Passkey welcome back' directly relates to the main change: updating the login success message and implementing passkey autofill flow with a 'welcome back' message.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/passkey-welcome-back-1d0f

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kentcdodds kentcdodds marked this pull request as ready for review February 23, 2026 20:55
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/routes/login.tsx`:
- Around line 246-259: In the catch block for the autofill/passkey flow, when an
unexpected error occurs (i.e., not NotAllowedError or AbortError) clear the
lingering passkey message and restore the form state so the user can recover:
inside the existing catch (after the isMounted check and excluding the
NotAllowedError/AbortError branch) call the state setters to clear
passkeyMessage (e.g., setPasskeyMessage('') or null), set a visible error (e.g.,
setLoginError(e.message || 'Passkey sign-in failed')), and re-enable the
form/submission state (e.g., setIsSubmitting(false) or setFormDisabled(false))
so the UI is no longer inert; keep these updates guarded by isMounted.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cb5747 and e0b32b0.

📒 Files selected for processing (4)
  • app/routes/login.tsx
  • app/routes/resources/webauthn/verify-authentication.ts
  • app/utils/webauthn.server.ts
  • e2e/passkey-form-autofill.spec.ts

Comment on lines +246 to +259
} catch (e) {
if (!isMounted) return

// Autofill flow should fail silently when the user cancels or chooses a
// password instead.
if (
e instanceof Error &&
(e.name === 'NotAllowedError' || e.name === 'AbortError')
) {
return
}

console.error(e)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clear the overlay on unexpected autofill failures.

For non-NotAllowedError/AbortError, passkeyMessage stays set and the form remains inert. Clear the message (and surface an error) so users can recover.

🐛 Proposed fix
 			if (
 				e instanceof Error &&
 				(e.name === 'NotAllowedError' || e.name === 'AbortError')
 			) {
 				return
 			}
 
 			console.error(e)
+			setPasskeyMessage(null)
+			setError(
+				e instanceof Error ? e.message : 'Failed to authenticate with passkey',
+			)
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (e) {
if (!isMounted) return
// Autofill flow should fail silently when the user cancels or chooses a
// password instead.
if (
e instanceof Error &&
(e.name === 'NotAllowedError' || e.name === 'AbortError')
) {
return
}
console.error(e)
}
} catch (e) {
if (!isMounted) return
// Autofill flow should fail silently when the user cancels or chooses a
// password instead.
if (
e instanceof Error &&
(e.name === 'NotAllowedError' || e.name === 'AbortError')
) {
return
}
console.error(e)
setPasskeyMessage(null)
setError(
e instanceof Error ? e.message : 'Failed to authenticate with passkey',
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 246 - 259, In the catch block for the
autofill/passkey flow, when an unexpected error occurs (i.e., not
NotAllowedError or AbortError) clear the lingering passkey message and restore
the form state so the user can recover: inside the existing catch (after the
isMounted check and excluding the NotAllowedError/AbortError branch) call the
state setters to clear passkeyMessage (e.g., setPasskeyMessage('') or null), set
a visible error (e.g., setLoginError(e.message || 'Passkey sign-in failed')),
and re-enable the form/submission state (e.g., setIsSubmitting(false) or
setFormDisabled(false)) so the UI is no longer inert; keep these updates guarded
by isMounted.

@cursor

This comment has been minimized.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/login.tsx (1)

277-321: ⚠️ Potential issue | 🟠 Major

Add explicit WebAuthnAbortService.cancelCeremony() call at the start of handlePasskeyLogin().

The conditional UI autofill ceremony (line 223) may still be active when the user clicks the manual passkey login button. Starting a new startAuthentication() ceremony while the autofill ceremony is pending can trigger NotAllowedError/InvalidStateError on some browsers. While the library auto-cancels prior ceremonies when you call startAuthentication() again, explicitly cancelling before the manual flow ensures the collision is prevented.

🛠️ Proposed fix
 async function handlePasskeyLogin() {
 	try {
+		WebAuthnAbortService.cancelCeremony()
 		setError(undefined)
 		setPasskeyMessage('Generating Authentication Options')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 277 - 321, The manual passkey flow can
collide with a running autofill ceremony; call
WebAuthnAbortService.cancelCeremony() at the start of handlePasskeyLogin()
before any startAuthentication() invocation (i.e., as the first action inside
handlePasskeyLogin), so any pending autofill ceremony is explicitly canceled;
wrap the cancel call appropriately (await or handle its promise/rejections)
before proceeding to setPasskeyMessage, fetch authentication options, or call
startAuthentication().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/login.tsx`:
- Around line 277-321: The manual passkey flow can collide with a running
autofill ceremony; call WebAuthnAbortService.cancelCeremony() at the start of
handlePasskeyLogin() before any startAuthentication() invocation (i.e., as the
first action inside handlePasskeyLogin), so any pending autofill ceremony is
explicitly canceled; wrap the cancel call appropriately (await or handle its
promise/rejections) before proceeding to setPasskeyMessage, fetch authentication
options, or call startAuthentication().

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0b32b0 and 8ebdc4b.

📒 Files selected for processing (1)
  • app/routes/login.tsx

@cursor cursor bot force-pushed the cursor/passkey-welcome-back-1d0f branch from 8ebdc4b to 4e1e015 Compare February 23, 2026 21:27
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/login.tsx (1)

195-327: ⚠️ Potential issue | 🟠 Major

Prevent autofill ceremonies from interfering with manual passkey login.

Only one navigator.credentials.get() request can be outstanding at a time. If the user clicks "Login with Passkey" while the autofill ceremony is pending, the autofill flow can still call startAuthentication() after the manual flow has begun, causing a hard browser error (Chromium) or race condition failure (iOS). The current WebAuthnAbortService.cancelCeremony() call in handlePasskeyLogin() may not be sufficient on iOS, where aborting and immediately starting a new ceremony can fail even after abort completes.

Add a shared guard (e.g., autofillCancelledRef) that prevents setupPasskeyAutofill() from calling startAuthentication() once manual login starts:

🔧 Suggested guard to prevent overlapping ceremonies
 const [passkeyAutofillSupported, setPasskeyAutofillSupported] =
 	React.useState(false)
+const autofillCancelledRef = React.useRef(false)

 React.useEffect(() => {
 	let isMounted = true
+	autofillCancelledRef.current = false

 	async function setupPasskeyAutofill() {
 		try {
 			const supports = await browserSupportsWebAuthnAutofill()
 			if (!supports) return
-			if (!isMounted) return
+			if (!isMounted || autofillCancelledRef.current) return
 			setPasskeyAutofillSupported(true)

 			const optionsResponse = await fetch(
 				'/resources/webauthn/generate-authentication-options',
 				{ method: 'POST' },
 			)
+			if (!isMounted || autofillCancelledRef.current) return
 			const json = await optionsResponse.json()
 			const { options } = AuthenticationOptionsSchema.parse(json)
+			if (!isMounted || autofillCancelledRef.current) return

 			const authResponse = await startAuthentication({
 				optionsJSON: options,
 				useBrowserAutofill: true,
 			})
@@
 	return () => {
 		isMounted = false
+		autofillCancelledRef.current = true
 		WebAuthnAbortService.cancelCeremony()
 	}
 }, [navigate, revalidate])

 async function handlePasskeyLogin() {
 	try {
+		autofillCancelledRef.current = true
 		// Avoid collisions with a pending conditional UI ceremony.
 		WebAuthnAbortService.cancelCeremony()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 195 - 327, The autofill and manual passkey
flows can race because both call startAuthentication(); add a shared guard to
prevent the autofill flow from proceeding once manual login begins: create a
React ref (e.g., autofillCancelledRef = React.useRef(false)) and check it inside
setupPasskeyAutofill before calling startAuthentication (and again after any
await that could allow the manual flow to start), and in handlePasskeyLogin set
autofillCancelledRef.current = true immediately before calling
WebAuthnAbortService.cancelCeremony() so the autofill path returns early and
never invokes startAuthentication; keep existing cancelCeremony calls and
unmount cleanup but use this guard to avoid overlapping ceremonies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/login.tsx`:
- Around line 195-327: The autofill and manual passkey flows can race because
both call startAuthentication(); add a shared guard to prevent the autofill flow
from proceeding once manual login begins: create a React ref (e.g.,
autofillCancelledRef = React.useRef(false)) and check it inside
setupPasskeyAutofill before calling startAuthentication (and again after any
await that could allow the manual flow to start), and in handlePasskeyLogin set
autofillCancelledRef.current = true immediately before calling
WebAuthnAbortService.cancelCeremony() so the autofill path returns early and
never invokes startAuthentication; keep existing cancelCeremony calls and
unmount cleanup but use this guard to avoid overlapping ceremonies.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ebdc4b and f23c635.

📒 Files selected for processing (4)
  • app/routes/login.tsx
  • app/routes/resources/webauthn/verify-authentication.ts
  • app/utils/webauthn.server.ts
  • e2e/passkey-form-autofill.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/routes/resources/webauthn/verify-authentication.ts

@cursor

This comment has been minimized.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/login.tsx (1)

311-314: ⚠️ Potential issue | 🟡 Minor

Incomplete type union — success branch is missing from the cast.

The type assertion only covers the error case. TypeScript treats verificationJson.status as the literal 'error', making verificationJson.status === 'error' trivially true, which can suppress type-checker warnings and mislead future editors. The equivalent cast in the autofill flow (Lines 243–245) correctly includes both union members.

🐛 Proposed fix
-		const verificationJson = (await verificationResponse.json()) as {
-			status: 'error'
-			error: string
-		}
+		const verificationJson = (await verificationResponse.json()) as
+			| { status: 'success' }
+			| { status: 'error'; error: string }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 311 - 314, The type assertion for
verificationJson (the result of verificationResponse.json()) only covers the
error shape and forces verificationJson.status to be the literal 'error'; update
the cast to the correct union that includes both the error and success shapes
(like the autofill flow's union) so TypeScript sees verificationJson.status as
'error' | 'ok' (and include any success fields expected), then adjust downstream
code to use the proper discriminated-union checks on verificationJson.status.
🧹 Nitpick comments (1)
app/routes/login.tsx (1)

218-222: Add response.ok guards before parsing JSON in both flows.

Both the autofill effect (optionsResponse at Line 218 and verificationResponse at Line 234) and handlePasskeyLogin (Line 290, Line 302) parse response JSON unconditionally. When the server returns 4xx/5xx, the body won't match the expected shape — AuthenticationOptionsSchema.parse will throw a Zod validation error (e.g., "Expected string, received undefined") that surfaces as a confusing user-visible message.

🔧 Proposed fix (applies identically to all four fetch call sites)
 				const optionsResponse = await fetch(
 					'/resources/webauthn/generate-authentication-options',
 					{ method: 'POST' },
 				)
+				if (!optionsResponse.ok) {
+					throw new Error('Failed to generate authentication options')
+				}
 				const json = await optionsResponse.json()
 				const verificationResponse = await fetch(
 					'/resources/webauthn/verify-authentication',
 					{
 						method: 'POST',
 						headers: { 'Content-Type': 'application/json' },
 						body: JSON.stringify(authResponse),
 					},
 				)
+				if (!verificationResponse.ok) {
+					throw new Error('Failed to verify passkey')
+				}

Apply the same two patches to the corresponding fetch calls in handlePasskeyLogin (Lines 290 and 302).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 218 - 222, The fetch calls currently parse
JSON unconditionally which can feed invalid shapes into
AuthenticationOptionsSchema.parse and surface confusing Zod errors; update the
four call sites (the autofill effect using optionsResponse and
verificationResponse, and inside handlePasskeyLogin) to check response.ok before
calling response.json(), and on non-ok either throw or return a handled error
(e.g., read text or json for error details and surface a clear message) so
AuthenticationOptionsSchema.parse only receives valid bodies; ensure you adjust
the branches that call AuthenticationOptionsSchema.parse (or subsequent logic)
to run only when the response was ok.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/login.tsx`:
- Around line 311-314: The type assertion for verificationJson (the result of
verificationResponse.json()) only covers the error shape and forces
verificationJson.status to be the literal 'error'; update the cast to the
correct union that includes both the error and success shapes (like the autofill
flow's union) so TypeScript sees verificationJson.status as 'error' | 'ok' (and
include any success fields expected), then adjust downstream code to use the
proper discriminated-union checks on verificationJson.status.

---

Nitpick comments:
In `@app/routes/login.tsx`:
- Around line 218-222: The fetch calls currently parse JSON unconditionally
which can feed invalid shapes into AuthenticationOptionsSchema.parse and surface
confusing Zod errors; update the four call sites (the autofill effect using
optionsResponse and verificationResponse, and inside handlePasskeyLogin) to
check response.ok before calling response.json(), and on non-ok either throw or
return a handled error (e.g., read text or json for error details and surface a
clear message) so AuthenticationOptionsSchema.parse only receives valid bodies;
ensure you adjust the branches that call AuthenticationOptionsSchema.parse (or
subsequent logic) to run only when the response was ok.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f23c635 and 323df94.

📒 Files selected for processing (1)
  • app/routes/login.tsx

@cursor
Copy link

cursor bot commented Feb 23, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Missing isMounted guard before starting WebAuthn ceremony
    • Added isMounted checks after option and verification fetches to avoid starting or finishing the ceremony after unmount.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/routes/login.tsx (1)

328-338: ⚠️ Potential issue | 🟡 Minor

handlePasskeyLogin surfaces a confusing error when the user intentionally cancels the passkey dialog.

When the WebAuthn dialog is dismissed, startAuthentication throws NotAllowedError. The autofill flow (lines 263–268) silently swallows it; the explicit button path does not — the catch block shows the raw browser error string (e.g. "The operation either timed out or was not allowed."). The finally block is correct (autofill re-arms unconditionally), but the user-visible error is misleading for a deliberate cancellation.

🐛 Suggested fix
 		} catch (e) {
 			setPasskeyMessage(null)
+			if (e instanceof Error && e.name === 'NotAllowedError') {
+				// User dismissed the dialog — fail silently, autofill is re-armed below.
+				return
+			}
 			console.error(e)
 			setError(
 				e instanceof Error ? e.message : 'Failed to authenticate with passkey',
 			)
 		} finally {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 328 - 338, The catch in handlePasskeyLogin
currently surfaces the raw browser error (from startAuthentication) when the
user cancels the WebAuthn prompt; detect NotAllowedError (or e.name ===
"NotAllowedError") in the catch and treat it as a user-cancel (clear or set a
friendly message via setPasskeyMessage and avoid calling setError with the raw
message), otherwise preserve the existing behavior (setError for other errors);
keep the finally behavior that calls setPasskeyAutofillResetKey to re-arm
autofill.
♻️ Duplicate comments (1)
app/routes/login.tsx (1)

258-275: Past review comment is resolved — unexpected autofill errors now correctly clear passkeyMessage and surface an error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 258 - 275, The catch block in the login
handler must silently ignore user-cancelled WebAuthn flows and otherwise clear
the passkey UI state and surface an error; update the catch clause in
app/routes/login.tsx (the try/catch around the passkey/auth flow) so that it
returns early for e instanceof Error with e.name === 'NotAllowedError' or
'AbortError', and for all other errors it calls setPasskeyMessage(null), logs
the error, and calls setError(e instanceof Error ? e.message : 'Failed to
authenticate with passkey').
🧹 Nitpick comments (1)
app/routes/login.tsx (1)

182-184: z.object() silently strips all server-sent WebAuthn options beyond challenge.

z.object({ challenge: z.string() }) uses Zod's default strip mode, so every field the server sends beyond challenge (userVerification, rpId, timeout, extensions, etc.) is discarded before startAuthentication sees optionsJSON. For the autofill/conditional-UI flow, empty allowCredentials is intentional, but silently dropping server-specified userVerification or rpId could produce a ceremony that diverges from server expectations.

♻️ Suggested fix — use `.passthrough()` to preserve server-sent options
 const AuthenticationOptionsSchema = z.object({
-	options: z.object({ challenge: z.string() }),
+	options: z.object({ challenge: z.string() }).passthrough(),
 }) satisfies z.ZodType<{ options: PublicKeyCredentialRequestOptionsJSON }>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routes/login.tsx` around lines 182 - 184, AuthenticationOptionsSchema
currently uses z.object({ challenge: z.string() }) which strips any server-sent
WebAuthn fields beyond challenge; update the schema to preserve extra properties
by calling .passthrough() on the inner object (keep the outer ZodType satisfy
line intact) so startAuthentication receives the full optionsJSON (including
userVerification, rpId, timeout, extensions, allowCredentials, etc.) instead of
silently dropping them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@app/routes/login.tsx`:
- Around line 328-338: The catch in handlePasskeyLogin currently surfaces the
raw browser error (from startAuthentication) when the user cancels the WebAuthn
prompt; detect NotAllowedError (or e.name === "NotAllowedError") in the catch
and treat it as a user-cancel (clear or set a friendly message via
setPasskeyMessage and avoid calling setError with the raw message), otherwise
preserve the existing behavior (setError for other errors); keep the finally
behavior that calls setPasskeyAutofillResetKey to re-arm autofill.

---

Duplicate comments:
In `@app/routes/login.tsx`:
- Around line 258-275: The catch block in the login handler must silently ignore
user-cancelled WebAuthn flows and otherwise clear the passkey UI state and
surface an error; update the catch clause in app/routes/login.tsx (the try/catch
around the passkey/auth flow) so that it returns early for e instanceof Error
with e.name === 'NotAllowedError' or 'AbortError', and for all other errors it
calls setPasskeyMessage(null), logs the error, and calls setError(e instanceof
Error ? e.message : 'Failed to authenticate with passkey').

---

Nitpick comments:
In `@app/routes/login.tsx`:
- Around line 182-184: AuthenticationOptionsSchema currently uses z.object({
challenge: z.string() }) which strips any server-sent WebAuthn fields beyond
challenge; update the schema to preserve extra properties by calling
.passthrough() on the inner object (keep the outer ZodType satisfy line intact)
so startAuthentication receives the full optionsJSON (including
userVerification, rpId, timeout, extensions, allowCredentials, etc.) instead of
silently dropping them.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 323df94 and b4041c1.

📒 Files selected for processing (1)
  • app/routes/login.tsx

cursoragent and others added 14 commits February 23, 2026 23:48
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor cursor bot force-pushed the cursor/passkey-welcome-back-1d0f branch from b4041c1 to b30f9ad Compare February 23, 2026 23:48
cursoragent and others added 2 commits February 23, 2026 23:52
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

cursoragent and others added 2 commits February 24, 2026 02:56
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds kentcdodds merged commit 74594ca into main Feb 24, 2026
8 checks passed
@kentcdodds kentcdodds deleted the cursor/passkey-welcome-back-1d0f branch February 24, 2026 06:13
cursor bot pushed a commit that referenced this pull request Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants