From d9d6a422a761ebf7bd536ac0d99f68f198d4f876 Mon Sep 17 00:00:00 2001 From: fadymak Date: Thu, 28 May 2026 13:28:39 +0200 Subject: [PATCH] feat(docs): passkey registration and authentication (#46340) Adds documentation for Passkey registration and authentication flows. --- .../NavigationMenu.constants.ts | 6 + apps/docs/content/guides/auth/passkeys.mdx | 244 ++++++++++++++++++ apps/docs/spec/cli_v1_config.yaml | 44 ++++ supa-mdx-lint/Rule003Spelling.toml | 10 +- 4 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 apps/docs/content/guides/auth/passkeys.mdx diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts index 9c8f760b612..1fd5b0e51a8 100644 --- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts +++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts @@ -755,6 +755,12 @@ export const auth: NavMenuConstant = { enabled: allAuthProvidersEnabled, }, + { + name: 'Passkey', + url: '/guides/auth/passkeys', + enabled: allAuthProvidersEnabled, + }, + { name: 'Social Login (OAuth)', url: '/guides/auth/social-login', diff --git a/apps/docs/content/guides/auth/passkeys.mdx b/apps/docs/content/guides/auth/passkeys.mdx new file mode 100644 index 00000000000..3165f7d4679 --- /dev/null +++ b/apps/docs/content/guides/auth/passkeys.mdx @@ -0,0 +1,244 @@ +--- +id: 'passkeys' +title: 'Passkey authentication' +subtitle: 'Allow users to sign in with passkeys (WebAuthn)' +--- + +[Passkeys](https://fidoalliance.org/passkeys/) are a passwordless credential built on the [WebAuthn](https://www.w3.org/TR/webauthn-3/) standard. The user proves possession of a private key stored on their device (or password manager) using biometrics, a PIN, or a hardware security key. The matching public key is registered with Supabase Auth and used to verify future sign-ins. Passkeys are phishing-resistant and remove the need to manage shared secrets. + + + +Passkey support is experimental. The API may change without notice. You must explicitly opt-in when creating the Supabase client. See [Enable in the client](#enable-in-the-client). + + + +## How does it work? + +Each sign-in or registration is a WebAuthn ceremony with three steps: + +1. **Options**: the client requests a challenge from Supabase Auth. +2. **Ceremony**: the browser invokes `navigator.credentials.create()` (registration) or `navigator.credentials.get()` (authentication), prompting the user for biometrics or a security key. +3. **Verify**: the signed response is sent back to Supabase Auth, which validates the challenge and either stores the new credential or issues a session. + +Supabase Auth uses [discoverable credentials](https://www.w3.org/TR/webauthn-3/#discoverable-credential) for sign-in. The user does not need to provide an email, phone, or username — the authenticator resolves the account from the credential it stores. + +Registering a passkey requires an existing, confirmed, non-anonymous user. Sign-in works for any user that has previously registered a passkey, provided their email or phone is confirmed and the account is not banned. + +## Enable passkey authentication + +### Dashboard + +Open the [Passkeys settings](/dashboard/project/_/auth/passkeys) from the **Authentication → Passkeys** section of the Dashboard, turn on **Enable Passkey authentication**, and fill in the WebAuthn [relying party](https://www.w3.org/TR/webauthn-3/#relying-party) details: +{/* supa-mdx-lint-disable-next-line Rule004ExcludeWords */} + +- **Relying Party Display Name**: a human-readable name for your application shown during the passkey prompt (for example, "My App"). +- **Relying Party ID**: the bare domain name for your application (for example, "example.com"). Do not include a scheme, port, or path. This determines which passkeys can be used. +- **Relying Party Origins**: comma-separated list of allowed origins (for example "https://example.com,https://app.example.com"). HTTPS is required except for loopback addresses ("localhost", "127.0.0.1", "[::1]"). Each origin's hostname must match or be a subdomain of the Relying Party ID. Up to 5 origins. + +The dashboard pre-fills these from your project's Site URL and project name. Adjust them if your production app is served from a different domain. + + + +Passkeys are cryptographically bound to the Relying Party (RP) ID they were registered against. Changing the RP ID makes every existing passkey unusable for sign-in, and users will need to register a new one. Pick the RP ID carefully before users start enrolling, and keep it stable once they do. + + + +### CLI + +Add the following to `supabase/config.toml`: + +```toml +[auth.passkey] +enabled = true + +[auth.webauthn] +rp_display_name = "My App" +rp_id = "example.com" +rp_origins = ["https://example.com", "https://app.example.com"] +``` + +The `[auth.webauthn]` section is required when `auth.passkey.enabled` is `true`. + +### Management API + +You can also configure passkeys via the [Management API](/docs/reference/api/introduction): + +```bash +# Get your access token from https://supabase.com/dashboard/account/tokens +export SUPABASE_ACCESS_TOKEN="your-access-token" +export PROJECT_REF="your-project-ref" + +# Read the current passkey configuration +curl -X GET "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + | jq '{passkey_enabled, webauthn_rp_id, webauthn_rp_display_name, webauthn_rp_origins}' + +# Enable passkeys and set the WebAuthn relying party +curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \ + -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "passkey_enabled": true, + "webauthn_rp_display_name": "My App", + "webauthn_rp_id": "example.com", + "webauthn_rp_origins": "https://example.com,https://app.example.com" + }' +``` + +## Enable in the client + + + +Passkey support is currently experimental and requires explicit opt-in as the API may change without notice. + + + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient(supabaseUrl, supabaseKey, { + auth: { + experimental: { passkey: true }, + }, +}) +``` + +## Register a passkey + +A user must be signed in before they can register a passkey. Typically, you call this from a security settings page, or directly after sign-up. + +`auth.registerPasskey()` runs the full WebAuthn ceremony. It fetches a challenge, invokes the browser API, and verifies the response with Supabase Auth. + +```ts +const { data, error } = await supabase.auth.registerPasskey() + +if (error) { + // User cancelled, browser doesn't support WebAuthn, or verification failed + console.error(error) +} else { + console.log('Registered passkey', data.id) +} +``` + +The returned `data` contains the new passkey's metadata: + +```ts +{ + id: string // UUID — use this to update or delete the passkey + friendly_name?: string // Derived from the authenticator's AAGUID + created_at: string +} +``` + +A friendly name is automatically derived from the authenticator's Authenticator Attestation GUID (AAGUID). For example, `iCloud Keychain`, `Google Password Manager`, `1Password`. Users can rename their passkey afterwards — see [Manage passkeys](#manage-passkeys). + +See the [`registerPasskey` reference](/docs/reference/javascript/auth-registerpasskey) for the full API. + +## Sign in with a passkey + +`auth.signInWithPasskey()` runs the full discoverable-credential authentication ceremony. The user picks an account from the authenticator's UI — your app does not need to ask for an email or phone number upfront. + +```ts +const { data, error } = await supabase.auth.signInWithPasskey() + +if (error) { + console.error(error) +} else { + // data.session and data.user are set; the client also dispatches a SIGNED_IN event + console.log('Signed in as', data.user?.email) +} +``` + +See the [`signInWithPasskey` reference](/docs/reference/javascript/auth-signinwithpasskey) for the full API. + +## Two-step API + +For native flows, custom UI, or full control over the WebAuthn ceremony, use the lower-level `auth.passkey` namespace. Each operation is split into "start" and "verify". + +Registration: + +```ts +const { data: options } = await supabase.auth.passkey.startRegistration() +// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library) +const credential = await runRegistrationCeremony(options.options) +await supabase.auth.passkey.verifyRegistration({ + challengeId: options.challenge_id, + credential, +}) +``` + +Authentication: + +```ts +const { data: options } = await supabase.auth.passkey.startAuthentication() +// Run the WebAuthn ceremony yourself (e.g.: using a native WebAuthn library) +const credential = await runAuthenticationCeremony(options.options) +const { data } = await supabase.auth.passkey.verifyAuthentication({ + challengeId: options.challenge_id, + credential, +}) +``` + +The `options` field returned from `startRegistration` and `startAuthentication` matches the [WebAuthn `PublicKeyCredentialCreationOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialcreationoptions) and [`PublicKeyCredentialRequestOptions`](https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions) shapes (with `ArrayBuffer` fields encoded as base64url). + +See the [`auth.passkey` reference](/docs/reference/javascript/auth-passkey-api) for the full API. + +## Manage passkeys + +List, rename, and delete the current user's passkeys: + +```ts +// List +const { data: passkeys } = await supabase.auth.passkey.list() +// [{ id, friendly_name, created_at, last_used_at? }, ...] + +// Rename +await supabase.auth.passkey.update({ + passkeyId: passkeys[0].id, + friendlyName: 'Work laptop', +}) + +// Delete +await supabase.auth.passkey.delete({ passkeyId: passkeys[0].id }) +``` + +`friendlyName` is limited to 120 characters. `last_used_at` is updated each time the passkey is used to sign in. + +See the [`auth.passkey` reference](/docs/reference/javascript/auth-passkey-api) for the full API. + +## Admin API + +Server-side admin endpoints let you inspect and revoke a user's passkeys. These require the project's secret key and must only be called from a trusted server. + +```ts +import { createClient } from '@supabase/supabase-js' + +const supabase = createClient(supabaseUrl, supabaseSecretKey, { + auth: { experimental: { passkey: true } }, +}) + +const { data } = await supabase.auth.admin.passkey.listPasskeys({ userId }) + +await supabase.auth.admin.passkey.deletePasskey({ userId, passkeyId }) +``` + +See the [`auth.admin.passkey` reference](/docs/reference/javascript/auth-admin-passkey-api) for the full API. + +## Error codes + +| Code | Meaning | +| ------------------------------- | -------------------------------------------------------------------------------- | +| `passkey_disabled` | Passkey sign-in is not enabled for this project. | +| `too_many_passkeys` | The user has reached the maximum number of passkeys allowed per account. | +| `webauthn_credential_exists` | This authenticator has already been registered to the account. | +| `webauthn_credential_not_found` | The credential in the assertion is not registered with Supabase Auth. | +| `webauthn_challenge_not_found` | The challenge ID is unknown or has already been consumed. | +| `webauthn_challenge_expired` | The challenge expired before the client returned a credential. | +| `webauthn_verification_failed` | The signature, attestation, or assertion did not validate against the challenge. | + +In addition, `signInWithPasskey()` returns the usual sign-in failure modes: `email_not_confirmed`, `phone_not_confirmed`, and `user_banned`. + +## Limitations + +- SSO users cannot register passkeys. +- Anonymous users cannot register passkeys — link an email or phone first. diff --git a/apps/docs/spec/cli_v1_config.yaml b/apps/docs/spec/cli_v1_config.yaml index bb10e2fdf10..8dc6cb29e0b 100644 --- a/apps/docs/spec/cli_v1_config.yaml +++ b/apps/docs/spec/cli_v1_config.yaml @@ -1439,6 +1439,50 @@ parameters: - name: 'Auth Multi-Factor Authentication' link: 'https://supabase.com/docs/guides/auth/auth-mfa' + - id: 'auth.passkey.enabled' + title: 'auth.passkey.enabled' + tags: ['auth'] + required: false + default: 'false' + description: | + Enable passkey sign-in and registration. Requires `auth.webauthn` to be configured. + links: + - name: 'Sign in with Passkey' + link: 'https://supabase.com/docs/guides/auth/passkeys' + + - id: 'auth.webauthn.rp_display_name' + title: 'auth.webauthn.rp_display_name' + tags: ['auth'] + required: false + default: '' + description: | + Human-readable name for the WebAuthn relying party shown to users during the passkey ceremony. Required when `auth.passkey.enabled` is `true`. + links: + - name: 'Sign in with Passkey' + link: 'https://supabase.com/docs/guides/auth/passkeys' + + - id: 'auth.webauthn.rp_id' + title: 'auth.webauthn.rp_id' + tags: ['auth'] + required: false + default: '' + description: | + WebAuthn relying party identifier. Must be a registrable suffix of every origin in `auth.webauthn.rp_origins` (for example `example.com` for `https://app.example.com`). Required when `auth.passkey.enabled` is `true`. + links: + - name: 'Sign in with Passkey' + link: 'https://supabase.com/docs/guides/auth/passkeys' + + - id: 'auth.webauthn.rp_origins' + title: 'auth.webauthn.rp_origins' + tags: ['auth'] + required: false + default: '[]' + description: | + List of origins your app is served from. The browser refuses the WebAuthn ceremony if the page origin is not in this list. Required when `auth.passkey.enabled` is `true`. + links: + - name: 'Sign in with Passkey' + link: 'https://supabase.com/docs/guides/auth/passkeys' + - id: 'auth.sessions.timebox' title: 'auth.sessions.timebox' tags: ['auth'] diff --git a/supa-mdx-lint/Rule003Spelling.toml b/supa-mdx-lint/Rule003Spelling.toml index 09cbdba4640..aab030e4956 100644 --- a/supa-mdx-lint/Rule003Spelling.toml +++ b/supa-mdx-lint/Rule003Spelling.toml @@ -22,6 +22,7 @@ allow_list = [ "[A-Z]{2,5}s?", "[A-Za-z0-9_-]+(\\.[A-Z-a-z0-9_-]+)+(\\/[A-Za-z0-9_-]+)*", "[Aa]dd-ons?", + "AAGUID", "[Aa]llowlists?", "[Aa]uditability", "[Aa]utomations?", @@ -34,7 +35,7 @@ allow_list = [ "BootFailure", "[Bb]reakpoints?", "[Bb]uilt-ins?", - "[Cc]atalogs?", + "[Cc]atalogs?", "[Cc]hangelogs?", "CircleCI", "[Cc]odebases?", @@ -50,7 +51,7 @@ allow_list = [ "[Cc]ryptography", "[Cc]ryptosystem", "[Cc]utover", - "[Dd]ata[Ff]rames?", + "[Dd]ata[Ff]rames?", "[Dd]atasets?", "[Dd]atasources?", "[Dd]atetime", @@ -59,7 +60,7 @@ allow_list = [ "[Dd]evs?", "[Dd]iff(s|ing|ed)?", "[Dd]ropdown", - "DuckDB", + "DuckDB", "EarlyDrop", "[Ee]m-dash", "[Ee]m-dashes", @@ -115,6 +116,7 @@ allow_list = [ "[Oo]vercommit(s|ted|ting)?", "[Pp]arallelization", "[Pp]arams?", + "[Pp]asskeys?", "[Pp]assthrough", "[Pp]laintext", "[Pp]olyfill(s|ed)?", @@ -320,7 +322,7 @@ allow_list = [ "PGAudit", "PGroonga", "PgBouncer", - "pgjwt", + "pgjwt", "PHI", "Pico", "PingIdentity",