Skip to content

[@clerk/electron] Passkey autofill runs as a modal prompt: OS passkey dialog opens as soon as sign-in mounts #9496

Description

@t3dotgg

Packages: @clerk/electron@0.0.24 (with @clerk/electron-passkeys@0.0.3), @clerk/clerk-js@6.25.13, @clerk/react@6.12.10, @clerk/shared@4.25.10

TL;DR: The Electron passkey provider reports passkey autofill as supported, but it executes the autofill request as a modal prompt. <SignIn> starts the autofill flow on mount, so an OS passkey dialog opens the instant the sign-in form renders, before any user intent.

Steps to reproduce:

  1. Electron app with createClerkBridge({ passkeys: true, renderer: { scheme, host } }) in the main process and <ClerkProvider passkeys={passkeys}> from @clerk/electron/react + @clerk/electron/passkeys in the renderer (app served from a custom scheme).
  2. Enable passkeys with autofill on the Clerk instance.
  3. Sign in once and register a passkey.
  4. Sign out, then call clerk.openSignIn() (or render <SignIn />).

Expected behavior:

Same as the browser: autofill is a background conditional-mediation request. Passkeys show up in the identifier field autocomplete. A passkey dialog appears only after the user picks one or clicks "Use passkey".

Actual behavior:

On macOS and Windows, the native OS passkey sheet opens immediately when the sign-in form mounts. On the renderer path (https-served Electron apps), Chromium's modal WebAuthn dialog opens instead. On Linux with a custom scheme, the flow fails silently with passkey_not_supported (no dialog, but the autofill attempt still errors).

Why (traced through the published dist of packages/electron):

  1. clerk-js's sign-in form checks __internal_isWebAuthnAutofillSupported() and, if true, auto-starts SignIn.authenticateWithPasskey({ flow: 'autofill' }), passing conditionalUI: true to __internal_getPublicCredentials.
  2. createPasskeys().isAutoFillSupported in the default auto mode returns isWebAuthnAutofillSupported() from @clerk/shared/webauthn, which is true in Electron's Chromium.
  3. createPasskeys().get destructures only { publicKeyOptions } and drops conditionalUI:
    • The renderer path hardcodes webAuthnGetCredential({ publicKeyOptions, conditionalUI: false }), which @clerk/shared maps to mediation: "optional" — a modal request.
    • A custom-scheme origin can never satisfy the RP ID in originSatisfiesRpId, so decidePath routes to the native bridge on macOS/Windows — also modal.

So the provider advertises quiet autofill and then fulfills it with a blocking dialog.

Suggested fix:

Either honor conditionalUI in get() when the renderer path can do real conditional mediation, or make isAutoFillSupported() return false whenever the request would resolve to a modal path. It already returns false for mode: 'native'; the auto mode reports the renderer's capability even when the request will route native or be forced non-conditional.

Workaround we are shipping in T3 Code (pingdotgg/t3code#7437):

import { passkeys as electronPasskeys } from '@clerk/electron/passkeys';

const passkeys = {
  ...electronPasskeys,
  isAutoFillSupported: () => Promise.resolve(false),
};

This stops the auto-prompt while keeping the explicit "Use passkey" button working.

🤖 Filed by Claude Code (Fable 5).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions