Capabilities · API
WebAuthn and passkeys
Published
The Web Authentication API registers a public-key credential bound to an authenticator through navigator.credentials.create({ publicKey }) and later proves possession of it through navigator.credentials.get({ publicKey }), so a PWA signs users in with a fingerprint, face, or device PIN instead of a password. A passkey is such a credential created as discoverable (residentKey: "required") and, on most platforms, synced by a credential manager such as iCloud Keychain or Google Password Manager; the signed data includes the origin and a fresh challenge, so a credential phished on a look-alike domain is unusable.
PublicKeyCredential shipped in Chrome 67, Edge 18, Firefox 60, and Safari 13; conditional mediation (passkey autofill) in Chrome 108, Safari 16, and Firefox 119; PublicKeyCredential.getClientCapabilities() in Chrome 133, Safari 17.4, and Firefox 135 (BCD api.PublicKeyCredential). Android WebView 131 exposes isConditionalMediationAvailable() but it resolves false because autofill integration is absent. Per-platform passkey sync coverage is tracked on passkeys.dev.
Syntax
Section titled “Syntax”navigator.credentials.create({ publicKey: creationOptions, signal })navigator.credentials.get({ publicKey: requestOptions, mediation, signal })
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()PublicKeyCredential.isConditionalMediationAvailable()PublicKeyCredential.getClientCapabilities()PublicKeyCredential.parseCreationOptionsFromJSON(json)PublicKeyCredential.parseRequestOptionsFromJSON(json)credential.toJSON()create() resolves with a PublicKeyCredential whose response is an AuthenticatorAttestationResponse (clientDataJSON, attestationObject, getPublicKey(), getTransports()); get() resolves with one whose response is an AuthenticatorAssertionResponse (clientDataJSON, authenticatorData, signature, userHandle). The static is…Available() methods resolve with booleans and need no gesture. Both ceremonies require a secure context; a cross-origin iframe additionally needs the publickey-credentials-create or publickey-credentials-get Permissions Policy feature.
Parameters
Section titled “Parameters”create() takes PublicKeyCredentialCreationOptions under publicKey; get() takes PublicKeyCredentialRequestOptions, plus the mediation member of the outer CredentialRequestOptions. Binary members are BufferSource; the JSON variants accept base64url strings.
| Dictionary | Member | Type | Required | Description |
|---|---|---|---|---|
| creation | rp |
PublicKeyCredentialRpEntity |
Yes | name (required) and id (defaults to the origin’s effective domain; must be a registrable suffix of it). |
| creation | user |
PublicKeyCredentialUserEntity |
Yes | id (1 to 64 bytes, opaque), name, displayName. |
| creation | challenge |
BufferSource |
Yes | Server-generated random bytes, echoed in clientDataJSON. |
| creation | pubKeyCredParams |
sequence<PublicKeyCredentialParameters> |
Yes | { type: "public-key", alg } entries; -7 (ES256) and -257 (RS256) cover current authenticators. |
| creation | timeout |
unsigned long |
No | Hint in milliseconds; the client may clamp it. |
| creation | excludeCredentials |
sequence<PublicKeyCredentialDescriptor> |
No | Credential IDs the user already has; a matching authenticator rejects with InvalidStateError. |
| creation | authenticatorSelection |
AuthenticatorSelectionCriteria |
No | authenticatorAttachment ("platform" or "cross-platform"), residentKey ("discouraged", "preferred", "required"), requireResidentKey (legacy boolean), userVerification ("required", "preferred" default, "discouraged"). |
| creation | hints |
sequence<DOMString> |
No | "security-key", "client-device", "hybrid", in preference order. |
| creation | attestation |
DOMString |
No, default "none" |
"none", "indirect", "direct", "enterprise". |
| creation | extensions |
AuthenticationExtensionsClientInputs |
No | For example credProps to learn whether the credential became discoverable. |
| request | challenge |
BufferSource |
Yes | Fresh server challenge. |
| request | rpId |
DOMString |
No | Must match the rp.id used at creation. |
| request | allowCredentials |
sequence<PublicKeyCredentialDescriptor> |
No | Leave empty to let the authenticator pick a discoverable credential (the passkey flow). |
| request | userVerification |
DOMString |
No, default "preferred" |
As above. |
| request | timeout, hints, extensions |
No | As for creation. | |
| outer | mediation |
CredentialMediationRequirement |
No, default "optional" |
"conditional" shows passkeys inside form autofill and waits silently; "required" forces a prompt; "silent" is rejected for publicKey. |
| outer | signal |
AbortSignal |
No | Aborting rejects the pending ceremony with AbortError. |
Exceptions
Section titled “Exceptions”| Exception | Condition |
|---|---|
NotAllowedError |
The caller origin is opaque; the document is cross-origin to an ancestor without the matching Permissions Policy feature; the call has no transient activation where the client requires one; the user declined, the timeout elapsed, or no eligible credential could be found and the user dismissed the dialog; or (for create()) the user agent has not recently mediated an authentication and does not grant consent. |
SecurityError |
The effective domain is not a valid domain, or rp.id / rpId is neither equal to nor a registrable suffix of it and the related-origins check (/.well-known/webauthn) fails. |
TypeError |
user.id is not between 1 and 64 bytes. |
NotSupportedError |
No entry of pubKeyCredParams names a type and algorithm the client supports. |
InvalidStateError |
An authenticator holds a credential listed in excludeCredentials and the user consented to it (the only authenticator error the client surfaces as itself). |
AbortError |
signal was aborted. |
EncodingError |
parseCreationOptionsFromJSON() or parseRequestOptionsFromJSON() met a value it cannot decode. |
Authenticator-level statuses other than InvalidStateError (ConstraintError when the authenticator lacks discoverable-credential storage for residentKey: "required" or lacks user verification, UnknownError on internal failure) are not surfaced by the specification’s client algorithm; the ceremony continues with other authenticators and ends in NotAllowedError at timeout. Chrome diverges and reports them directly (see the observed detail).
Examples
Section titled “Examples”Every example feature-detects PublicKeyCredential and shows the branch that keeps password sign-in working where passkeys are unavailable. Challenges, user IDs, and credential IDs come from the server as base64url strings and are passed through the JSON helpers where the browser has them.
Registering a passkey with a password-form fallback
Section titled “Registering a passkey with a password-form fallback”Ask the server for creation options, check that a platform authenticator exists, and post the attestation back. Browsers before Chrome 129, Firefox 119, and Safari 18.4 lack parseCreationOptionsFromJSON(), so decode base64url by hand there.
const fromB64url = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
async function registerPasskey() { if (!window.PublicKeyCredential) return showPasswordForm('This browser has no passkey support.'); if (!(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())) { return showPasswordForm('No platform authenticator on this device.'); }
const json = await (await fetch('/webauthn/register/options', { method: 'POST' })).json(); const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON ? PublicKeyCredential.parseCreationOptionsFromJSON(json) : { ...json, challenge: fromB64url(json.challenge), user: { ...json.user, id: fromB64url(json.user.id) }, excludeCredentials: (json.excludeCredentials ?? []).map((c) => ({ ...c, id: fromB64url(c.id) })), };
try { const credential = await navigator.credentials.create({ publicKey }); await fetch('/webauthn/register/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(credential.toJSON ? credential.toJSON() : serialize(credential)), }); } catch (err) { if (err.name === 'InvalidStateError') return notify('A passkey for this account already exists on this device.'); if (err.name === 'NotAllowedError') return notify('Passkey creation was cancelled.'); throw err; }}The server-side options should set authenticatorSelection: { residentKey: "required", userVerification: "preferred" } so the credential is discoverable; otherwise the user must type a username before get() can find it.
Signing in with passkey autofill and a modal fallback
Section titled “Signing in with passkey autofill and a modal fallback”mediation: "conditional" lets the browser list passkeys inside the username field’s autofill. The request waits until the user picks one, so start it on page load with an AbortSignal that a visible “Sign in with a passkey” button cancels before issuing a modal request.
async function startConditionalSignIn(input, button) { if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return; if (!(await PublicKeyCredential.isConditionalMediationAvailable())) return;
input.setAttribute('autocomplete', 'username webauthn'); const controller = new AbortController(); const options = await (await fetch('/webauthn/login/options')).json(); const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
button.addEventListener('click', async () => { controller.abort(); const assertion = await navigator.credentials.get({ publicKey, mediation: 'required' }); await verify(assertion); });
try { const assertion = await navigator.credentials.get({ publicKey, mediation: 'conditional', signal: controller.signal }); await verify(assertion); } catch (err) { if (err.name !== 'AbortError') console.error(`${err.name}: ${err.message}`); }}Leave allowCredentials empty so any passkey stored for rpId can appear; users who have none see ordinary autofill, so the password path stays untouched.
Choosing between a platform passkey and a security key
Section titled “Choosing between a platform passkey and a security key”hints tells the browser which UI to lead with. For a workforce app that issues hardware keys, lead with "security-key" and require user verification; for a consumer app, "client-device" keeps the device’s own biometrics first.
async function createFor(audience, publicKey) { if (!window.PublicKeyCredential) throw new Error('WebAuthn unavailable'); const options = { ...publicKey, hints: audience === 'workforce' ? ['security-key', 'hybrid'] : ['client-device', 'hybrid'], authenticatorSelection: { residentKey: 'required', userVerification: audience === 'workforce' ? 'required' : 'preferred', }, }; return navigator.credentials.create({ publicKey: options });}Browsers without hints support drop the member under WebIDL dictionary rules, so the fallback is automatic: the default chooser appears instead.
See also
Section titled “See also”- Credential Management API, the container these calls live in
- PWAs on iOS and Safari, where passkeys sync through iCloud Keychain
- Web Authentication Level 3: create a new credential (w3.org)
- Web Authentication Level 3: get an assertion (w3.org)
- Create a passkey for passwordless logins (web.dev)
- Sign in with a passkey through form autofill (web.dev)
- Device support for passkeys (passkeys.dev)
Specifications
| Specification | Status |
|---|---|
| Web Authentication Level 3: create a new credential | W3C |
| Web Authentication Level 3: get an assertion | W3C |
| Web Authentication Level 3: AuthenticatorSelectionCriteria | W3C |