Skip to content

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.

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.

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.
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).

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.

Specifications

SpecificationStatus
Web Authentication Level 3: create a new credentialW3C
Web Authentication Level 3: get an assertionW3C
Web Authentication Level 3: AuthenticatorSelectionCriteriaW3C