# WebAuthn and passkeys

> navigator.credentials.create() and get() with publicKey register and verify passkeys: every option member, the DOMExceptions, and runnable sign-in examples.

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](https://passkeys.dev/device-support/).

## Syntax

```js
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

`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

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

:::observed
Chrome maps authenticator outcomes to fixed messages in [`authentication_credentials_container.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc) (chromium.googlesource.com): dismissing the passkey dialog or letting it time out rejects with `NotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.`; an `rp.id` that is not a suffix of the origin rejects with `SecurityError: This is an invalid domain.`; a hit on `excludeCredentials` rejects with `InvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.`; calling while the tab is unfocused rejects with `NotAllowedError: The operation is not allowed at this time because the page does not have focus.`. The DevTools panel named **WebAuthn** (English UI; under More tools) adds a virtual authenticator so these paths can be exercised without hardware ([Chrome DevTools: Emulate authenticators](https://developer.chrome.com/docs/devtools/webauthn) (developer.chrome.com)).
:::

## 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

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.

```js
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

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

```js
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

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

```js
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

- [Credential Management API](/reference/capabilities/credential-management/), the container these calls live in
- [PWAs on iOS and Safari](/reference/platforms/ios-safari/), where passkeys sync through iCloud Keychain
- [Web Authentication Level 3: create a new credential](https://www.w3.org/TR/webauthn-3/#sctn-createCredential) (w3.org)
- [Web Authentication Level 3: get an assertion](https://www.w3.org/TR/webauthn-3/#sctn-getAssertion) (w3.org)
- [Create a passkey for passwordless logins](https://web.dev/articles/passkey-registration) (web.dev)
- [Sign in with a passkey through form autofill](https://web.dev/articles/passkey-form-autofill) (web.dev)
- [Device support for passkeys](https://passkeys.dev/device-support/) (passkeys.dev)