Skip to content

feat(auth): forward options.mediation to navigator.credentials.get in signInWithPasskey - #2675

Open
itsybitsci wants to merge 2 commits into
supabase:masterfrom
itsybitsci:feat/passkey-mediation
Open

feat(auth): forward options.mediation to navigator.credentials.get in signInWithPasskey#2675
itsybitsci wants to merge 2 commits into
supabase:masterfrom
itsybitsci:feat/passkey-mediation

Conversation

@itsybitsci

Copy link
Copy Markdown

🔍 Description

Lets signInWithPasskey() opt into WebAuthn Conditional UI (passkey autofill) by forwarding a mediation option to navigator.credentials.get().

What changed?

  • SignInWithPasskeyCredentials.options gains mediation?: CredentialMediationRequirement (the lib.dom type, so no new imports).
  • GoTrueClient.signInWithPasskey passes credentials?.options?.mediation through to getCredential, which already accepts every CredentialRequestOptions field and hands them to navigator.credentials.get() unchanged. Nothing else in the ceremony moves.
  • TSDoc on the new option and on signInWithPasskey.
  • Tests in passkey.methods.test.ts: one asserting mediation: 'conditional' reaches navigator.credentials.get and the ceremony still completes, and an assertion in the existing full-ceremony test that mediation stays undefined when not requested.

Why was this change needed?

Today signInWithPasskey only reads captchaToken and signal, so the browser's modal picker is the only path. Conditional UI lets the browser surface passkeys in the autofill prompt on field focus or page load, which removes a tap and, on iOS Safari, shows the native passkey suggestion above the keyboard. The only missing piece in the SDK was the mediation pass-through; the autocomplete="username webauthn" input is app-side.

Closes #2672

📸 Screenshots/Examples

// <input autocomplete="username webauthn" /> on the page
const { data, error } = await supabase.auth.signInWithPasskey({
  options: { mediation: 'conditional' },
})

🔄 Breaking changes

  • This PR contains no breaking changes

The option is optional and omitted by default, so existing callers keep the modal behavior. The passkey API is still behind auth.experimental.passkey.

📋 Checklist

  • I have read the Contributing Guidelines
  • My PR title follows the conventional commit format: <type>(<scope>): <description>
  • I have run pnpm nx format to ensure consistent code formatting
  • I have added tests for new functionality (if applicable)
  • I have updated documentation (if applicable)

📝 Additional notes

Deliberately a plain pass-through. The issue also floats a guard that drops mediation when PublicKeyCredential.isConditionalMediationAvailable() reports no support; I left that out because silently ignoring an explicit option is debatable and browsers already handle unsupported values themselves. Happy to add it if preferred. registerPasskey is untouched since CredentialCreationOptions has no mediation in lib.dom.

… signInWithPasskey

Add an optional `mediation` field to the signInWithPasskey options and pass
it through to navigator.credentials.get(), so apps can opt into WebAuthn
Conditional UI (passkey autofill) with `mediation: 'conditional'`. The
option is omitted by default, so existing callers keep the modal picker.

Closes supabase#2672
@itsybitsci
itsybitsci requested review from a team as code owners September 7, 2026 10:18
@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: fcea1645-bcdd-4903-8d1d-eb1db32f466a

📥 Commits

Reviewing files that changed from the base of the PR and between 2e447d2 and bbc573d.

📒 Files selected for processing (4)
  • packages/core/auth-js/src/GoTrueClient.ts
  • packages/core/auth-js/src/lib/error-codes.ts
  • packages/core/auth-js/src/lib/types.ts
  • packages/core/auth-js/test/passkey.methods.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/auth-js/src/GoTrueClient.ts
  • packages/core/auth-js/src/lib/types.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Summary

Summary by CodeRabbit

  • New Features

    • Passkey sign-in now supports optional credential mediation settings.
    • When enabled with conditional, passkeys can appear through WebAuthn autofill on supported browsers.
    • When omitted, sign-in continues using the browser’s standard modal experience.
  • Documentation

    • Added guidance describing the new mediation option and the input configuration required for passkey autofill.
  • Tests

    • Added coverage for conditional passkey autofill and confirmed the default sign-in behavior remains unchanged.

Walkthrough

The passkey sign-in API now accepts an optional options.mediation value. signInWithPasskey forwards this value to navigator.credentials.get(). The type definition and JSDoc document conditional WebAuthn UI. Tests verify both the default behavior and forwarding of 'conditional' while preserving the existing passkey ceremony.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant GoTrueClient
  participant AuthAPI
  participant WebAuthn
  App->>GoTrueClient: signInWithPasskey({ options: { mediation } })
  GoTrueClient->>AuthAPI: request passkey options
  GoTrueClient->>WebAuthn: navigator.credentials.get({ publicKey, signal, mediation })
  WebAuthn-->>GoTrueClient: passkey credential
  GoTrueClient->>AuthAPI: verify passkey credential
  AuthAPI-->>GoTrueClient: session response
Loading

Assessment against linked issues

Objective Addressed Explanation
[#2672] Accept and forward options.mediation to navigator.credentials.get().
[#2672] Preserve default modal behavior when mediation is omitted.
[#2672] Preserve the existing passkey options and verification flow.

Priority: ⬇️ Low — Defer the optional WebAuthn mediation change because it is a focused auth API enhancement that preserves existing sign-in behavior and has no stated urgent customer or incident impact.

Merge Risk: ⚪ Minimal · up to bbc57

Passkey sign-in now supports optional WebAuthn mediation while preserving the default picker behavior. Conditional mediation forwarding and retry cancellation are covered, with no active merge-blocking risk identified.


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.

@mandarini mandarini 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.

Hi @itsybitsci, thank you so much for contributing to Supabase! 💚

The passthrough itself looks correct and nicely minimal. One thing to sort out before merging: the authentication challenge from _startPasskeyAuthentication expires after 5 minutes by default, but conditional UI is designed to sit pending indefinitely until the user picks a passkey from autofill. If they take longer than that, the browser ceremony succeeds but verify will fail with an expired-challenge error.

Could you add a line to the TSDoc calling this out, and let us know your take on the retry path, should the app call signInWithPasskey again on that error, and does the browser handle a second conditional getCredential call while one may still be pending?

Thank you again for contributing, this is a genuinely useful addition.

…ign-in

The authentication challenge expires server-side (5 minutes by default)
while a conditional UI prompt can stay pending indefinitely, so a late
pick fails verification with webauthn_challenge_expired. Document this on
signInWithPasskey and the mediation option, explain that calling
signInWithPasskey again cancels the pending ceremony and starts a fresh
challenge, add the error code to the ErrorCode union, and cover the retry
path with a test.
@itsybitsci

Copy link
Copy Markdown
Author

Hi @itsybitsci, thank you so much for contributing to Supabase! 💚

The passthrough itself looks correct and nicely minimal. One thing to sort out before merging: the authentication challenge from _startPasskeyAuthentication expires after 5 minutes by default, but conditional UI is designed to sit pending indefinitely until the user picks a passkey from autofill. If they take longer than that, the browser ceremony succeeds but verify will fail with an expired-challenge error.

Could you add a line to the TSDoc calling this out, and let us know your take on the retry path, should the app call signInWithPasskey again on that error, and does the browser handle a second conditional getCredential call while one may still be pending?

Thank you again for contributing, this is a genuinely useful addition.

Thanks for the review @mandarini , and good catch on the expiry.

Pushed a follow-up:

  • TSDoc on signInWithPasskey and on options.mediation now spells out that the challenge expires after the server's GOTRUE_WEBAUTHN_CHALLENGE_EXPIRY_DURATION (5 minutes by default), that a late pick from the autofill prompt fails verification with error_code: "webauthn_challenge_expired", and how to recover.
  • Added webauthn_challenge_expired to the ErrorCode union so error.code === 'webauthn_challenge_expired' type-checks for the retry branch.

On the retry path: calling signInWithPasskey() again is the right move, and the SDK already handles the "still pending" case. When the caller doesn't pass their own signal, each call takes a fresh signal from the shared webAuthnAbortService, whose createNewAbortSignal() aborts the in-flight navigator.credentials.get() before the new one starts. So the browser never sees two concurrent WebAuthn requests, the stale conditional prompt is cancelled and re-armed with a fresh challenge, and the earlier promise resolves with a WebAuthnError whose code is ERROR_CEREMONY_ABORTED. The one caveat is an app that passes its own signal, which bypasses the service and must abort the previous ceremony itself; the TSDoc says so.

Added a test that runs exactly that sequence: a pending conditional get(), a second signInWithPasskey() call, the first resolving with ERROR_CEREMONY_ABORTED, the second completing against the new challenge_id with a different signal.

@itsybitsci
itsybitsci requested a review from mandarini September 8, 2026 13:05

@mandarini mandarini 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.

thanks

@pkg-pr-new

pkg-pr-new Bot commented Sep 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2675

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2675

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2675

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2675

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2675

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2675

commit: bbc573d

@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 74.628% (-7.5%) from 82.084% — itsybitsci:feat/passkey-mediation into supabase:master

@mandarini mandarini self-assigned this Sep 8, 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.

signInWithPasskey: allow mediation: 'conditional' for WebAuthn Conditional UI (autofill)

3 participants