Skip to content

feat(clerk-js,ui,shared,localizations): Support passkey as a second factor#9127

Open
dmoerner wants to merge 1 commit into
mainfrom
daniel/user-3966-passkey-as-2fa-option
Open

feat(clerk-js,ui,shared,localizations): Support passkey as a second factor#9127
dmoerner wants to merge 1 commit into
mainfrom
daniel/user-3966-passkey-as-2fa-option

Conversation

@dmoerner

@dmoerner dmoerner commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

The Clerk backend can now offer passkey in a sign-in's (and session reverification's) supported_second_factors when the instance allows passkeys to satisfy the second factor and the user has a registered passkey. This adds client support end to end:

  • Types: passkey variants in SignInSecondFactor, Prepare/AttemptSecondFactorParams, SessionVerificationSecondFactor, and the session verify param unions.
  • signIn.authenticateWithPasskey() detects needs_second_factor (and needs_client_trust) and runs the prepare/attempt second-factor flow against the in-progress sign-in instead of creating a new one; the flow param is ignored there. attemptSecondFactor serializes the WebAuthn credential like attemptFirstFactor. The future API gains mfa.passkey().
  • session.verifyWithPasskey({ level: 'second_factor' }) completes a multi-factor step-up with a passkey.
  • Prebuilt UI: passkey cards for the sign-in factor-two and user-verification factor-two steps, offered via the existing totp/phone-first ladder (passkey is only the starting factor when it is the sole option), listed under "Use another method", and hidden when WebAuthn is unsupported. Fixes the alternative-methods label switch that threw on unknown second-factor strategies.
  • New signIn.passkeyMfa / reverification.passkeyMfa localization keys.

Part of USER-3966

Description

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

Summary by CodeRabbit

  • New Features

    • Added passkey support as a second factor for sign-in and session re-verification.
    • Added passkey options to prebuilt SignIn and User Verification flows.
    • Added passkey MFA support to the sign-in API.
    • Passkey options are shown only when WebAuthn is supported.
    • Improved passkey handling for reset-password flows.
  • Localization

    • Added passkey MFA translation keys across supported locales.
  • Tests

    • Added coverage for passkey sign-in, re-verification, availability, and UI behavior.

…actor

The Clerk backend can now offer passkey in a sign-in's (and session
reverification's) supported_second_factors when the instance allows
passkeys to satisfy the second factor and the user has a registered
passkey. This adds client support end to end:

- Types: passkey variants in SignInSecondFactor,
  Prepare/AttemptSecondFactorParams, SessionVerificationSecondFactor,
  and the session verify param unions.
- signIn.authenticateWithPasskey() detects needs_second_factor (and
  needs_client_trust) and runs the prepare/attempt second-factor flow
  against the in-progress sign-in instead of creating a new one; the
  flow param is ignored there. attemptSecondFactor serializes the
  WebAuthn credential like attemptFirstFactor. The future API gains
  mfa.passkey().
- session.verifyWithPasskey({ level: 'second_factor' }) completes a
  multi-factor step-up with a passkey.
- Prebuilt UI: passkey cards for the sign-in factor-two and
  user-verification factor-two steps, offered via the existing
  totp/phone-first ladder (passkey is only the starting factor when it
  is the sole option), listed under "Use another method", and hidden
  when WebAuthn is unsupported. Fixes the alternative-methods label
  switch that threw on unknown second-factor strategies.
- New signIn.passkeyMfa / reverification.passkeyMfa localization keys.

Part of USER-3966

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 707e4ad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@clerk/shared Minor
@clerk/clerk-js Minor
@clerk/ui Minor
@clerk/localizations Minor
@clerk/astro Patch
@clerk/backend Patch
@clerk/chrome-extension Patch
@clerk/electron Patch
@clerk/expo-passkeys Patch
@clerk/expo Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/headless Patch
@clerk/hono Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/vue Patch
@clerk/swingset Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 10, 2026 8:26pm
swingset Ready Ready Preview, Comment Jul 10, 2026 8:26pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Passkey second-factor support is added to sign-in and session reverification APIs, shared types, prebuilt sign-in and user-verification UIs, tests, localization resources, and release metadata.

Changes

Passkey second-factor support

Layer / File(s) Summary
Contracts and resource flows
packages/shared/src/..., packages/clerk-js/src/core/resources/...
Shared types, errors, session verification, sign-in MFA methods, WebAuthn credential serialization, and first-/second-factor API flows are extended for passkeys.
Sign-in UI
packages/ui/src/components/SignIn/..., packages/ui/src/utils/..., packages/ui/src/test/...
Sign-in factor selection, WebAuthn capability filtering, passkey cards, reset-password completion routing, sorting, fixtures, and tests are updated.
User verification UI
packages/ui/src/components/UserVerification/...
User verification renders and submits passkey second-factor flows, filters unsupported strategies, aborts pending WebAuthn operations, and tests the new behavior.
Localization and release metadata
packages/localizations/src/*, .changeset/passkey-second-factor.md
Passkey MFA localization keys are added across locales, and package minor bumps with release notes are recorded.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PasskeyCard
  participant ClerkResource
  participant WebAuthn
  User->>PasskeyCard: Select passkey second factor
  PasskeyCard->>ClerkResource: prepare second-factor verification
  ClerkResource-->>PasskeyCard: return passkey nonce
  PasskeyCard->>WebAuthn: request credential
  WebAuthn-->>PasskeyCard: return publicKeyCredential
  PasskeyCard->>ClerkResource: attempt second-factor verification
  ClerkResource-->>PasskeyCard: return verification response
Loading

Poem

A rabbit taps a passkey bright,
Through sign-in fields and moonlit night.
WebAuthn hops, the nonce sings,
MFA grows two sturdy wings.
New words bloom in every locale—
“Hop verified!” says Bunny’s console.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: end-to-end passkey support as a second factor across clerk-js, ui, shared, and localizations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9127

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9127

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9127

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9127

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9127

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9127

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9127

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9127

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9127

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9127

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9127

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9127

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9127

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9127

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9127

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9127

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9127

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9127

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9127

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9127

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9127

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9127

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9127

commit: 707e4ad

@github-actions

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-07-10T20:29:36.963Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 1
🔴 Breaking changes 0
🟡 Non-breaking changes 9
🟢 Additions 2

🤖 This report was reviewed by claude-sonnet-4-6.


@clerk/shared

Current version: 4.25.2
Recommended bump: MINOR → 4.26.0

Subpath ./internal/clerk-js/errors

🟢 Additions (1)

Added: clerkMissingPasskeySecondFactor
+ declare function clerkMissingPasskeySecondFactor(): never;

Added function clerkMissingPasskeySecondFactor

Subpath ./types

🟡 Non-breaking Changes (9)

Modified: __internal_LocalizationResource
Diff (before: 1980 lines, after: 1989 lines). Click to expand.
// ... 485 unchanged lines elided ...
        title: LocalizationValue;
        subtitle: LocalizationValue;
      };
+     passkeyMfa: {
+       title: LocalizationValue;
+       subtitle: LocalizationValue;
+     };
      alternativeMethods: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        actionLink: LocalizationValue;
        actionText: LocalizationValue;
        blockButton__emailLink: LocalizationValue<'identifier'>;
        blockButton__emailCode: LocalizationValue<'identifier'>;
        blockButton__phoneCode: LocalizationValue<'identifier'>;
        blockButton__password: LocalizationValue;
        blockButton__passkey: LocalizationValue;
        blockButton__totp: LocalizationValue;
        blockButton__backupCode: LocalizationValue;
        getHelp: {
          title: LocalizationValue;
          content: LocalizationValue;
          blockButton__emailSupport: LocalizationValue;
        };
      };
      noAvailableMethods: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        message: LocalizationValue;
      };
      accountSwitcher: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        action__addAccount: LocalizationValue;
        action__signOutAll: LocalizationValue;
      };
      enterpriseConnections: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
      };
      web3Solana: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
      };
      protectCheck: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        loading: LocalizationValue;
        retryButton: LocalizationValue;
      };
    };
    reverification: {
      password: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        actionLink: LocalizationValue;
      };
      emailCode: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        formTitle: LocalizationValue;
        resendButton: LocalizationValue;
      };
      phoneCode: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        formTitle: LocalizationValue;
        resendButton: LocalizationValue;
      };
      phoneCodeMfa: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        formTitle: LocalizationValue;
        resendButton: LocalizationValue;
      };
      totpMfa: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        formTitle: LocalizationValue;
      };
      backupCodeMfa: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
      };
      passkey: {
+       title: LocalizationValue;
+       subtitle: LocalizationValue;
+       blockButton__passkey: LocalizationValue;
+     };
+     passkeyMfa: {
        title: LocalizationValue;
        subtitle: LocalizationValue;
        blockButton__passkey: LocalizationValue;
// ... 1411 unchanged lines elided ...

Static analyzer: Breaking change in type alias __internal_LocalizationResource: Type changed: {locale:string;maintenanceMode:import("@clerk/shared").LocalizationValue;roles:{[r:string]:import("@clerk/shared").Loca…{locale:string;maintenanceMode:import("@clerk/shared").LocalizationValue;roles:{[r:string]:import("@clerk/shared").Loca…

🤖 AI review (reclassified as non-breaking) (70%): The before and after snippets show the same structural shape with only the elided line count changing (1900 vs 1909 lines), indicating new optional localization string fields were added. LocalizationResource is used as DeepPartial<DeepLocalizationWithoutObjects<__internal_LocalizationResource>>, so consumers only provide partial subsets and are not required to supply newly added keys.

Modified: AttemptSecondFactorParams
- type AttemptSecondFactorParams = PhoneCodeAttempt | TOTPAttempt | BackupCodeAttempt | EmailCodeAttempt;
+ type AttemptSecondFactorParams = PhoneCodeAttempt | TOTPAttempt | BackupCodeAttempt | EmailCodeAttempt | PasskeyAttempt;

Static analyzer: Breaking change in type alias AttemptSecondFactorParams: Type changed: import("@clerk/shared").BackupCodeAttempt|import("@clerk/shared").EmailCodeAttempt|import("@clerk/shared").PhoneCodeAtt…import("@clerk/shared").BackupCodeAttempt|import("@clerk/shared").EmailCodeAttempt|import("@clerk/shared").PasskeyAttem…

🤖 AI review (reclassified as non-breaking) (95%): AttemptSecondFactorParams is used as a parameter type (input) for SignInResource.attemptSecondFactor; adding PasskeyAttempt to the union widens the accepted input, which is non-breaking for callers passing existing attempt types.

Modified: PrepareSecondFactorParams
- type PrepareSecondFactorParams = PhoneCodeSecondFactorConfig | EmailCodeSecondFactorConfig | EmailLinkConfig;
+ type PrepareSecondFactorParams = PhoneCodeSecondFactorConfig | EmailCodeSecondFactorConfig | EmailLinkConfig | PassKeyConfig;

Static analyzer: Breaking change in type alias PrepareSecondFactorParams: Type changed: import("@clerk/shared").EmailCodeSecondFactorConfig|import("@clerk/shared").EmailLinkConfig|import("@clerk/shared").Pho…import("@clerk/shared").EmailCodeSecondFactorConfig|import("@clerk/shared").EmailLinkConfig|import("@clerk/shared").Pas…

🤖 AI review (reclassified as non-breaking) (95%): PrepareSecondFactorParams is used as a parameter type (input) for SignInResource.prepareSecondFactor; adding PassKeyConfig to the union widens the accepted input, which is non-breaking for existing callers.

Modified: SessionResource.verifyWithPasskey
- verifyWithPasskey: () => Promise<SessionVerificationResource>;
+ verifyWithPasskey: (params?: SessionVerifyWithPasskeyParams) => Promise<SessionVerificationResource>;

Static analyzer: Breaking change in property SessionResource.verifyWithPasskey: Type changed: ()=>!Promise:interface<import("@clerk/shared").SessionVerificationResource>(params?:import("@clerk/shared").SessionVerifyWithPasskeyParams)=>!Promise:interface<import("@clerk/shared").SessionVer…

🤖 AI review (reclassified as non-breaking) (95%): Adding an optional parameter params?: SessionVerifyWithPasskeyParams to verifyWithPasskey is non-breaking: existing call sites that pass no arguments remain valid, and the parameter is optional so no adaptation is required.

Modified: SessionVerificationSecondFactor
- type SessionVerificationSecondFactor = PhoneCodeFactor | TOTPFactor | BackupCodeFactor;
+ type SessionVerificationSecondFactor = PhoneCodeFactor | TOTPFactor | BackupCodeFactor | PasskeyFactor;

Static analyzer: Breaking change in type alias SessionVerificationSecondFactor: Type changed: import("@clerk/shared").BackupCodeFactor|import("@clerk/shared").PhoneCodeFactor|import("@clerk/shared").TOTPFactorimport("@clerk/shared").BackupCodeFactor|import("@clerk/shared").PasskeyFactor|import("@clerk/shared").PhoneCodeFactor|…

🤖 AI review (reclassified as non-breaking) (95%): SessionVerificationSecondFactor is used as the element type of the output field supportedSecondFactors on SessionVerificationResource; adding PasskeyFactor to this output union is non-breaking for consumers who read the array.

Modified: SessionVerifyAttemptSecondFactorParams
- type SessionVerifyAttemptSecondFactorParams = PhoneCodeAttempt | TOTPAttempt | BackupCodeAttempt;
+ type SessionVerifyAttemptSecondFactorParams = PhoneCodeAttempt | TOTPAttempt | BackupCodeAttempt | PasskeyAttempt;

Static analyzer: Breaking change in type alias SessionVerifyAttemptSecondFactorParams: Type changed: import("@clerk/shared").BackupCodeAttempt|import("@clerk/shared").PhoneCodeAttempt|import("@clerk/shared").TOTPAttemptimport("@clerk/shared").BackupCodeAttempt|import("@clerk/shared").PasskeyAttempt|import("@clerk/shared").PhoneCodeAttem…

🤖 AI review (reclassified as non-breaking) (95%): SessionVerifyAttemptSecondFactorParams is used as a parameter type (input) for SessionResource.attemptSecondFactorVerification; adding PasskeyAttempt to the union widens the accepted input, which is non-breaking for existing callers.

Modified: SessionVerifyPrepareSecondFactorParams
- type SessionVerifyPrepareSecondFactorParams = PhoneCodeSecondFactorConfig;
+ type SessionVerifyPrepareSecondFactorParams = PhoneCodeSecondFactorConfig | PassKeyConfig;

Static analyzer: Breaking change in type alias SessionVerifyPrepareSecondFactorParams: Type changed: import("@clerk/shared").PhoneCodeSecondFactorConfigimport("@clerk/shared").PassKeyConfig|import("@clerk/shared").PhoneCodeSecondFactorConfig

🤖 AI review (reclassified as non-breaking) (95%): SessionVerifyPrepareSecondFactorParams is used as a parameter type (input) for SessionResource.prepareSecondFactorVerification; adding PassKeyConfig to the union widens the accepted input, which is non-breaking for existing callers.

Modified: SignInFutureResource.mfa
// ... 16 unchanged lines elided ...
      verifyBackupCode: (params: SignInFutureBackupCodeVerifyParams) => Promise<{
        error: ClerkError | null;
      }>;
+     passkey: () => Promise<{
+       error: ClerkError | null;
+     }>;
    };

Static analyzer: Breaking change in property SignInFutureResource.mfa: Type changed: {sendPhoneCode:()=>!Promise:interface<{error:import("@clerk/shared").~ClerkError|null;}>;verifyPhoneCode:(params:import…{sendPhoneCode:()=>!Promise:interface<{error:import("@clerk/shared").~ClerkError|null;}>;verifyPhoneCode:(params:import…

🤖 AI review (reclassified as non-breaking) (90%): The change adds a new passkey method to the mfa object on SignInFutureResource; this is a purely additive change to an output/result object and does not affect existing well-typed consumers.

Modified: SignInSecondFactor
- type SignInSecondFactor = PhoneCodeFactor | TOTPFactor | BackupCodeFactor | EmailCodeFactor | EmailLinkFactor;
+ type SignInSecondFactor = PhoneCodeFactor | TOTPFactor | BackupCodeFactor | EmailCodeFactor | EmailLinkFactor | PasskeyFactor;

Static analyzer: Breaking change in type alias SignInSecondFactor: Type changed: import("@clerk/shared").BackupCodeFactor|import("@clerk/shared").EmailCodeFactor|import("@clerk/shared").EmailLinkFacto…import("@clerk/shared").BackupCodeFactor|import("@clerk/shared").EmailCodeFactor|import("@clerk/shared").EmailLinkFacto…

🤖 AI review (reclassified as non-breaking) (90%): SignInSecondFactor is used in output positions (supportedSecondFactors on SignInResource and SignInFutureResource) and in SignInFactor/SignInSecondFactorJSON type aliases; adding PasskeyFactor to the union widens an output type, which is non-breaking for consumers reading these values.

🟢 Additions (1)

Added: SessionVerifyWithPasskeyParams
+ type SessionVerifyWithPasskeyParams = {
+   level?: 'first_factor' | 'second_factor';
+ };

Added type alias SessionVerifyWithPasskeyParams


Report generated by Break Check

Last ran on 707e4ad.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx (1)

407-424: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test may pass for the wrong reason — wrap in mockWebAuthn to test priority ordering.

Without mockWebAuthn, isWebAuthnSupported() likely returns false in the test environment, so passkey is excluded from candidacy regardless of priority sorting against TOTP. This test currently can't distinguish "TOTP wins on priority" from "passkey is filtered out due to no WebAuthn support." Wrapping this in mockWebAuthn(() => { ... }) (as done for the sibling tests below) would ensure the sorting/priority logic is actually exercised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx` around
lines 407 - 424, Update the Passkey test “is never the starting factor when
another second factor is available” to wrap its fixture setup, render, and
assertions in mockWebAuthn, matching the sibling tests below. Keep supportTotp
and supportPasskey enabled so the test validates priority ordering rather than
passkey being excluded by unsupported WebAuthn.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts`:
- Around line 261-264: Restore direct static assignments in the passkey test
suites: update the authenticateWithPasskey and mfa.passkey cleanup to save the
original BaseResource._fetch and SignIn.clerk values before stubbing, then
reassign those originals in afterEach alongside vi.clearAllMocks() and
vi.unstubAllGlobals().

In `@packages/clerk-js/src/core/resources/Session.ts`:
- Around line 320-329: The verifyWithPasskey method silently treats invalid
levels as first-factor verification. Default level with nullish coalescing, then
explicitly validate that it is either 'first_factor' or 'second_factor' and
reject any other value before calling prepareFirstFactorVerification or
prepareSecondFactorVerification.
- Around line 320-329: Document the public verifyWithPasskey API with JSDoc
directly above the verifyWithPasskey method in Session, describing the
params.level options and default, the returned SessionVerificationResource, and
possible error behavior; flag the contract change for Docs-team review or
accompanying generated documentation updates.

In `@packages/clerk-js/src/core/resources/SignIn.ts`:
- Around line 575-610: Add or update JSDoc for the public
authenticateWithPasskey method to document that needs_second_factor and
needs_client_trust sign-ins reuse the in-progress sign-in via the second-factor
flow, and that the flow option is ignored in those states. Include the API
behavior change clearly and mark the documentation for Docs-team review.

In `@packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx`:
- Around line 32-41: Update handlePasskeysAttempt to guard against concurrent
passkey attempts: set the card’s loading state before calling verifyWithPasskey,
clear it in a finally handler regardless of success or failure, and disable the
passkey button while loading so additional clicks cannot abort the in-flight
ceremony.

---

Nitpick comments:
In `@packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx`:
- Around line 407-424: Update the Passkey test “is never the starting factor
when another second factor is available” to wrap its fixture setup, render, and
assertions in mockWebAuthn, matching the sibling tests below. Keep supportTotp
and supportPasskey enabled so the test validates priority ordering rather than
passkey being excluded by unsupported WebAuthn.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: c030f53b-d9cd-46b4-8a0f-e7f81cb887a2

📥 Commits

Reviewing files that changed from the base of the PR and between 7b581aa and 707e4ad.

📒 Files selected for processing (73)
  • .changeset/passkey-second-factor.md
  • packages/clerk-js/src/core/resources/Session.ts
  • packages/clerk-js/src/core/resources/SignIn.ts
  • packages/clerk-js/src/core/resources/__tests__/Session.test.ts
  • packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
  • packages/localizations/src/ar-SA.ts
  • packages/localizations/src/be-BY.ts
  • packages/localizations/src/bg-BG.ts
  • packages/localizations/src/bn-IN.ts
  • packages/localizations/src/ca-ES.ts
  • packages/localizations/src/cs-CZ.ts
  • packages/localizations/src/da-DK.ts
  • packages/localizations/src/de-DE.ts
  • packages/localizations/src/el-GR.ts
  • packages/localizations/src/en-GB.ts
  • packages/localizations/src/en-US.ts
  • packages/localizations/src/es-CR.ts
  • packages/localizations/src/es-ES.ts
  • packages/localizations/src/es-MX.ts
  • packages/localizations/src/es-UY.ts
  • packages/localizations/src/fa-IR.ts
  • packages/localizations/src/fi-FI.ts
  • packages/localizations/src/fr-FR.ts
  • packages/localizations/src/he-IL.ts
  • packages/localizations/src/hi-IN.ts
  • packages/localizations/src/hr-HR.ts
  • packages/localizations/src/hu-HU.ts
  • packages/localizations/src/id-ID.ts
  • packages/localizations/src/is-IS.ts
  • packages/localizations/src/it-IT.ts
  • packages/localizations/src/ja-JP.ts
  • packages/localizations/src/kk-KZ.ts
  • packages/localizations/src/ko-KR.ts
  • packages/localizations/src/mn-MN.ts
  • packages/localizations/src/ms-MY.ts
  • packages/localizations/src/nb-NO.ts
  • packages/localizations/src/nl-BE.ts
  • packages/localizations/src/nl-NL.ts
  • packages/localizations/src/pl-PL.ts
  • packages/localizations/src/pt-BR.ts
  • packages/localizations/src/pt-PT.ts
  • packages/localizations/src/ro-RO.ts
  • packages/localizations/src/ru-RU.ts
  • packages/localizations/src/sk-SK.ts
  • packages/localizations/src/sr-RS.ts
  • packages/localizations/src/sv-SE.ts
  • packages/localizations/src/ta-IN.ts
  • packages/localizations/src/te-IN.ts
  • packages/localizations/src/th-TH.ts
  • packages/localizations/src/tr-TR.ts
  • packages/localizations/src/uk-UA.ts
  • packages/localizations/src/vi-VN.ts
  • packages/localizations/src/zh-CN.ts
  • packages/localizations/src/zh-TW.ts
  • packages/shared/src/internal/clerk-js/errors.ts
  • packages/shared/src/types/localization.ts
  • packages/shared/src/types/session.ts
  • packages/shared/src/types/sessionVerification.ts
  • packages/shared/src/types/signInCommon.ts
  • packages/shared/src/types/signInFuture.ts
  • packages/ui/src/components/SignIn/SignInClientTrust.tsx
  • packages/ui/src/components/SignIn/SignInFactorTwo.tsx
  • packages/ui/src/components/SignIn/SignInFactorTwoAlternativeMethods.tsx
  • packages/ui/src/components/SignIn/SignInFactorTwoPasskeyCard.tsx
  • packages/ui/src/components/SignIn/__tests__/SignInFactorTwo.test.tsx
  • packages/ui/src/components/SignIn/shared.ts
  • packages/ui/src/components/SignIn/utils.ts
  • packages/ui/src/components/UserVerification/UVFactorTwoAlternativeMethods.tsx
  • packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx
  • packages/ui/src/components/UserVerification/UserVerificationFactorTwo.tsx
  • packages/ui/src/components/UserVerification/__tests__/UVFactorTwo.test.tsx
  • packages/ui/src/test/fixture-helpers.ts
  • packages/ui/src/utils/factorSorting.ts

Comment on lines +261 to +264
afterEach(() => {
vi.clearAllMocks();
vi.unstubAllGlobals();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
ast-grep outline packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts \
  --items all --type function --match 'beforeEach|afterEach' --view expanded

rg -n -C4 \
  'beforeEach\s*\(|afterEach\s*\(|BaseResource\._fetch\s*=|SignIn\.clerk\s*=' \
  packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts

Repository: clerk/javascript

Length of output: 40759


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Context around the first passkey suite =="
sed -n '220,360p' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | cat -n

echo
echo "== Context around the mfa.passkey suite =="
sed -n '1880,2015p' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | cat -n

echo
echo "== Search for global restore/cleanup in this test file =="
rg -n 'restoreAllMocks|resetAllMocks|clearAllMocks|unstubAllGlobals|BaseResource\._fetch =|SignIn\.clerk =|const originalFetch|const originalClerk|afterEach\(\s*\(\)\s*=>\s*\{' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts

Repository: clerk/javascript

Length of output: 16958


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Surrounding structure for authenticateWithPasskey and nested passkey suite =="
sed -n '150,340p' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | cat -n

echo
echo "== Surrounding structure for authenticateWithPasskey's later nested suites =="
sed -n '1560,1995p' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | cat -n

echo
echo "== AST outline for describe blocks in SignIn.test.ts =="
ast-grep outline packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts --view expanded --match 'describe($_, $_)'

Repository: clerk/javascript

Length of output: 24915


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Top of SignIn.test.ts for file-level hooks =="
sed -n '1,120p' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | cat -n

echo
echo "== Any restore/assignment hooks outside the target suites =="
rg -n -C2 'beforeEach\s*\(|afterEach\s*\(|BaseResource\._fetch =|SignIn\.clerk =' packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts | sed -n '1,120p'

Repository: clerk/javascript

Length of output: 8497


Restore the patched statics in these passkey suites

BaseResource._fetch and SignIn.clerk are assigned directly here, but afterEach only clears mocks and unstubs globals. Save the originals and restore them in afterEach for authenticateWithPasskey and mfa.passkey, or use a cleanup that restores direct assignments too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts` around lines
261 - 264, Restore direct static assignments in the passkey test suites: update
the authenticateWithPasskey and mfa.passkey cleanup to save the original
BaseResource._fetch and SignIn.clerk values before stubbing, then reassign those
originals in afterEach alongside vi.clearAllMocks() and vi.unstubAllGlobals().

Source: Coding guidelines

Comment on lines +320 to +329
verifyWithPasskey = async (params?: SessionVerifyWithPasskeyParams): Promise<SessionVerificationResource> => {
const level = params?.level || 'first_factor';

const { nonce = null } = prepareResponse.firstFactorVerification;
const prepareResponse =
level === 'second_factor'
? await this.prepareSecondFactorVerification({ strategy: 'passkey' })
: await this.prepareFirstFactorVerification({ strategy: 'passkey' });

const { nonce = null } =
level === 'second_factor' ? prepareResponse.secondFactorVerification : prepareResponse.firstFactorVerification;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject invalid verification levels instead of silently downgrading.

Line 321 uses ||, while Lines 324-329 treat every value except 'second_factor' as first factor. Invalid JavaScript/config input can therefore request step-up verification but receive lower-assurance first-factor verification. Use ?? for the default and explicitly reject values outside the two supported levels.

As per coding guidelines, “Validate all inputs” and “Follow OWASP security best practices.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/resources/Session.ts` around lines 320 - 329, The
verifyWithPasskey method silently treats invalid levels as first-factor
verification. Default level with nullish coalescing, then explicitly validate
that it is either 'first_factor' or 'second_factor' and reject any other value
before calling prepareFirstFactorVerification or
prepareSecondFactorVerification.

Source: Coding guidelines


📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new public verification-level contract.

Add JSDoc covering params.level, its default, return value, and error behavior. This API change may also require Docs-team review.

As per coding guidelines, “All public APIs must be documented with JSDoc” and “Update documentation for API changes.” As per path instructions, public API JSDoc is customer-facing generated documentation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/resources/Session.ts` around lines 320 - 329,
Document the public verifyWithPasskey API with JSDoc directly above the
verifyWithPasskey method in Session, describing the params.level options and
default, the returned SessionVerificationResource, and possible error behavior;
flag the contract change for Docs-team review or accompanying generated
documentation updates.

Sources: Coding guidelines, Path instructions

Comment on lines +575 to +610
// When the sign-in already awaits its second factor, the passkey acts as
// the second factor: run the discrete prepare/attempt second-factor flow
// against the in-progress sign-in. `flow` is ignored here — autofill and
// discoverable are identifier-first concepts, and their `create()` call
// would discard the in-progress sign-in.
const isSecondFactor = this.status === 'needs_second_factor' || this.status === 'needs_client_trust';
if (isSecondFactor) {
const passkeySecondFactor = (this.supportedSecondFactors || []).find(f => f.strategy === 'passkey');
if (!passkeySecondFactor) {
clerkMissingPasskeySecondFactor();
}

await this.prepareSecondFactor({ strategy: 'passkey' });

const { nonce: secondFactorNonce } = this.secondFactorVerification;
const secondFactorPublicKeyOptions = secondFactorNonce
? convertJSONToPublicKeyRequestOptions(JSON.parse(secondFactorNonce))
: null;
if (!secondFactorPublicKeyOptions) {
clerkMissingWebAuthnPublicKeyOptions('get');
}

const { publicKeyCredential, error } = await webAuthnGetCredential({
publicKeyOptions: secondFactorPublicKeyOptions,
conditionalUI: false,
});
if (!publicKeyCredential) {
throw error;
}

return this.attemptSecondFactor({
publicKeyCredential,
strategy: 'passkey',
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document the new second-factor behavior of authenticateWithPasskey.

The public method now reuses in-progress second-factor/client-trust sign-ins and ignores flow in that state. Add JSDoc describing these semantics and request Docs-team review.

As per coding guidelines, “All public APIs must be documented with JSDoc” and “Update documentation for API changes.” As per path instructions, changed public API JSDoc may affect generated Clerk Docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/clerk-js/src/core/resources/SignIn.ts` around lines 575 - 610, Add
or update JSDoc for the public authenticateWithPasskey method to document that
needs_second_factor and needs_client_trust sign-ins reuse the in-progress
sign-in via the second-factor flow, and that the flow option is ignored in those
states. Include the API behavior change clearly and mark the documentation for
Docs-team review.

Sources: Coding guidelines, Path instructions

Comment on lines +32 to +41
const handlePasskeysAttempt = () => {
session
?.verifyWithPasskey({ level: 'second_factor' })
.then(response => {
return handleVerificationResponse(response);
})
.catch(err => handleError(err, [], card.setError));

return;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail
printf 'Repo root: '; pwd
git ls-files 'packages/ui/src/components/UserVerification/*passkey*' 'packages/ui/src/components/UserVerification/*Card.tsx' 'packages/ui/src/**/Button*' | sed 's#^`#-` #'

Repository: clerk/javascript

Length of output: 672


🏁 Script executed:

set -euo pipefail
# Map relevant files and inspect their size before reading.
for f in \
  packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx \
  packages/ui/src/components/UserVerification/UVFactorSignInPasskeyCard.tsx \
  packages/ui/src/components/UserVerification/UVFactorPasskeyCard.tsx \
  packages/ui/src/components/UserVerification/*Passkey*Card.tsx \
  packages/ui/src/components/Button.tsx \
  packages/ui/src/components/**/Button.tsx
do
  if [ -f "$f" ]; then
    printf '\n== %s ==\n' "$f"
    wc -l "$f"
  fi
done

Repository: clerk/javascript

Length of output: 612


🏁 Script executed:

set -euo pipefail

echo '--- outline: UVFactorTwoPasskeyCard.tsx ---'
ast-grep outline packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx --view expanded || true

echo '--- outline: UVFactorOnePasskeysCard.tsx ---'
ast-grep outline packages/ui/src/components/UserVerification/UVFactorOnePasskeysCard.tsx --view expanded || true

echo '--- outline: Button.tsx ---'
ast-grep outline packages/ui/src/primitives/Button.tsx --view expanded || true

echo '--- targeted reads ---'
sed -n '1,140p' packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx
printf '\n====\n'
sed -n '1,140p' packages/ui/src/components/UserVerification/UVFactorOnePasskeysCard.tsx
printf '\n====\n'
sed -n '1,220p' packages/ui/src/primitives/Button.tsx

Repository: clerk/javascript

Length of output: 13913


🏁 Script executed:

set -euo pipefail

echo '--- search card state loading usage ---'
rg -n "setLoading|isLoading|useCardState\(" packages/ui/src -g '!**/dist/**' -g '!**/build/**'

echo '--- locate useCardState definition ---'
rg -n "export .*useCardState|function useCardState|const useCardState" packages/ui/src

echo '--- inspect relevant contexts files ---'
fd -a "contexts" packages/ui/src | sed -n '1,80p'

Repository: clerk/javascript

Length of output: 50372


🏁 Script executed:

set -euo pipefail

echo '--- SignInFactorTwoPasskeyCard.tsx outline ---'
ast-grep outline packages/ui/src/components/SignIn/SignInFactorTwoPasskeyCard.tsx --view expanded || true

echo '--- SignInFactorTwoPasskeyCard.tsx ---'
sed -n '1,220p' packages/ui/src/components/SignIn/SignInFactorTwoPasskeyCard.tsx

echo '--- card state context ---'
sed -n '1,140p' packages/ui/src/elements/contexts/index.tsx

Repository: clerk/javascript

Length of output: 6458


🏁 Script executed:

set -euo pipefail
sed -n '1,220p' packages/ui/src/components/SignIn/shared.ts

Repository: clerk/javascript

Length of output: 5560


packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx: add a loading guard before starting another passkey attempt. This card calls verifyWithPasskey() directly, so a second click can abort the in-flight ceremony and surface a passkey_operation_aborted error here. Set loading before the request, clear it in finally, and disable the button while the attempt is pending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/UserVerification/UVFactorTwoPasskeyCard.tsx`
around lines 32 - 41, Update handlePasskeysAttempt to guard against concurrent
passkey attempts: set the card’s loading state before calling verifyWithPasskey,
clear it in a finally handler regardless of success or failure, and disable the
passkey button while loading so additional clicks cannot abort the in-flight
ceremony.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant