Passkeys make user accounts safer, simpler, easier to use.
Using passkeys enhances security, simplifies logins, and replaces passwords. Unlike regular passwords, which users must remember and enter manually, passkeys use device's screen lock mechanisms like biometrics or PINs and reduce phishing risks and credential theft.
Passkeys sync across devices using passkey providers like Google Password Manager and iCloud Keychain.
A passkey must be created, storing the private key securely to the passkey provider along with necessary metadata and its public key stored on your server for authentication. The private key issues a signature after user verification on the valid domain making passkeys phishing resistant. The public key verifies the signature without storing sensitive credentials, making passkeys resistant to credential theft.
How creating a passkey works
Before a user can sign in with a passkey, you should create the passkey, associate it with a user account, and store its public key on your server.
You could ask users to create a passkey in one of the following situations:
- During or after sign up.
- After signing in.
- After signing in using a passkey from another device (that is, the
[authenticatorAttachment](https://web.dev/articles/passkey-form-autofill#authenticator-attachment)
iscross-platform
). - On a dedicated page where users can manage their passkeys.
To create a passkey, you use the WebAuthn API.
The four components of the passkey registration flow are:
- Backend: Stores user account details, including the public key.
- Frontend: Communicates with the browser and fetches necessary data from the backend.
- Browser: Runs your JavaScript and interacts with the WebAuthn API.
- Passkey provider: Creates and stores the passkey. This is typically a password manager such as Google Password Manager, or a security key.

Before creating a passkey, ensure that the system meets these prerequisites:
The user account is verified through a secure method (for example, email, phone verification, or identity federation) within a meaningfully short window.
The frontend and backend can communicate securely to exchange credential data.
The browser supports WebAuthn and passkey creation.
We can show you how to check most of them in the following sections.
Once the system meets this conditions, the following process happens to create a passkey:
- The system triggers the passkey creation process when the user initiates the action (for example, clicking a "Create a Passkey" button in their passkey management page or after finishing their registration).
- The frontend requests necessary credential data from the backend, including user information, a challenge, and credential IDs to prevent duplicates.
- The frontend calls
navigator.credentials.create()
to prompt the device's passkey provider to generate a passkey using the information from the backend. Note that this call returns a promise. - The user's device authenticates the user using a biometric method, PIN, or pattern to create the passkey.
- The passkey provider creates a passkey and returns a public key credential to the frontend, resolving the promise.
- The frontend sends the generated public key credential to the backend.
- The backend stores the public key and other important data for future authentication,
- The backend notifies the user (for example, using email) to confirm the passkey creation and detect potential unauthorized access.
This process ensures a secure and seamless passkey registration process for users.
Compatibilities
Most browsers support WebAuthn, with some minor gaps. See passkeys.dev for browser and OS compatibility details.
Create a new passkey
To create a new passkey, this is the process the frontend should follow:
- Check for compatibility.
- Fetch information from the backend.
- Call WebAuth API to create a passkey.
- Send the returned public key to the backend.
- Save the credential.
The following sections show how you can do it.
Check for compatibility
Before displaying a "Create a new passkey" button, the frontend should check if:
- The browser supports WebAuthn with
PublicKeyCredential
.
- The device supports a platform authenticator (can create a passkey and
authenticate with the passkey) with
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
.
- The browser supports WebAuthn conditional
UI with
PublicKeyCredenital.isConditionalMediationAvailable()
.
The following code snippet shows how you can check for compatibility before displaying the passkey-related options.
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.
// `isConditionalMediationAvailable` means the feature detection is usable.
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
// Check if user verifying platform authenticator is available.
Promise.all([
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
PublicKeyCredential.isConditionalMediationAvailable(),
]).then(results => {
if (results.every(r => r === true)) {
// Display "Create a new passkey" button
}
});
}
In this example, the Create a new passkey button should only be displayed if all the conditions are met.
Fetch information from the backend
When the user clicks the button, fetch the required information from the
backend to call navigator.credentials.create()
.
The following code snippet shows a JSON object with the required information to
call navigator.credentials.create()
:
// Example `PublicKeyCredentialCreationOptions` contents
{
challenge: *****,
rp: {
name: "Example",
id: "example.com",
},
user: {
id: *****,
name: "john78",
displayName: "John",
},
pubKeyCredParams: [{
alg: -7, type: "public-key"
},{
alg: -257, type: "public-key"
}],
excludeCredentials: [{
id: *****,
type: 'public-key',
transports: ['internal'],
}],
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
}
}
The key-value pairs in the object hold the following information:
challenge
: A server-generated challenge in ArrayBuffer for this registration.rp.id
: An RP ID (Relying Party ID), a domain and a website can specify either its domain or a registrable suffix. For example, if an RP's origin ishttps://login.example.com:1337
, the RP ID can be eitherlogin.example.com
orexample.com
. If the RP ID is specified asexample.com
, the user can authenticate onlogin.example.com
or on any subdomains onexample.com
. See, Allow passkey reuse across your sites with Related Origin Requests for more information on this.rp.name
: The RP's (Relying Party) name. This is deprecated in WebAuthn L3 but included for compatibility reasons.user.id
: A unique user ID in ArrayBuffer, generated upon account creation. It should be permanent, unlike a username that may be editable. The user ID identifies an account, but should not contain any personally identifiable information (PII). You likely already have a user ID in your system, but if needed, create one specifically for passkeys to keep it free of any PII.user.name
: A unique identifier for the account that the user will recognise, like their email address or username. This will be displayed in the account selector.user.displayName
: A required, more user-friendly name for the account. It need not be unique and could be the user's chosen name. If your site does not have a suitable value to include here, pass an empty string. This may be displayed on the account selector depending on the browser.pubKeyCredParams
: Specifies the RP (relying party) supported public-key algorithms. We recommend setting it to[{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]
. This specifies support for ECDSA with P-256 and RSA PKCS#1 and supporting these gives complete coverage.excludeCredentials
: A list of already registered credential IDs. Prevents registering the same device twice by providing a list of already registered credential IDs. Thetransports
member, if provided, should contain the result of callinggetTransports()
during the registration of each credential.authenticatorSelection.authenticatorAttachment
: Set this to"platform"
along withhint: ['client-device']
if this passkey creation is an upgrade from a password for example in a promotion after a sign-in."platform"
indicates that the RP wants a platform authenticator (an authenticator embedded to the platform device) which does not prompt, for example, to insert a USB security key. The user has a simpler option to create a passkey.authenticatorSelection.requireResidentKey
: Set it to a booleantrue
. A discoverable credential (resident key) stores user information to the passkey and lets users select the account upon authentication.authenticatorSelection.userVerification
: Indicates whether a user verification using the device screen lock is"required"
,"preferred"
or"discouraged"
. The default is"preferred"
, which means the authenticator may skip user verification. Set this to"preferred"
or omit the property.
We recommend constructing the object on the server, encoding the ArrayBuffer
with Base64URL and fetching it from the frontend. This way, you can decode the
payload using PublicKeyCredential.parseCreationOptionsFromJSON()
and pass it
directly to navigator.credentials.create()
.
The following code snippet shows how you can fetch and decode the information needed to create the passkey.
// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');
// Deserialize and decode the `PublicKeyCredentialCreationOptions`.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(decoded_options);
...
Call WebAuthn API to create a passkey
Call navigator.credentials.create()
to create a new passkey. The API returns
a promise, waiting for the user's interaction displaying a modal dialog.
// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
publicKey: options
});
Send the returned public key credential to the backend
After the user is verified using the device's screen lock, a passkey is created and the promise is resolved returning a PublicKeyCredential object to the frontend.
The promise can be rejected for different reasons. You can handle these errors
by checking the Error
object's name
property:
InvalidStateError
: A passkey already exists on the device. No error dialog will be shown to the user. The site shouldn't treat this as an error. The user wanted the local device registered and it is.NotAllowedError
: The user has canceled the operation.AbortError
: The operation has been aborted.- Other exceptions: Something unexpected happened. The browser shows an error dialog to the user.
The public key credential object contains the following properties:
id
: A Base64URL encoded ID of the created passkey. This ID helps the browser to determine whether a matching passkey is in the device upon authentication. This value must be stored in the database on the backend.rawId
: An ArrayBuffer version of the credential ID.response.clientDataJSON
: An ArrayBuffer encoded client data.response.attestationObject
: An ArrayBuffer encoded attestation object. This contains important information such as an RP ID, flags and a public key.authenticatorAttachment
: Returns"platform"
when this credential is created on a passkey capable device.type
: This field is always set to"public-key"
.
Encode the object with the .toJSON()
method, serialize it with
JSON.stringify()
then send it to the server.
...
// Encode and serialize the `PublicKeyCredential`.
const _result = credential.toJSON();
const result = JSON.stringify(_result);
// Encode and send the credential to the server for verification.
const response = await fetch('/webauthn/registerResponse', {
method: 'post',
credentials: 'same-origin',
body: result
});
...
Save the credential
After receiving the public key credential on the backend, we recommend using a server-side library or a solution instead of writing your own code to process a public-key credential.
You can then store the information retrieved from the credential to the database for future use.
The following list includes recommended properties to save:
- Credential ID: The credential ID returned with the public key credential.
- Credential name: The name of the credential. Name it after the passkey provider it's created by which can be identified based on the AAGUID.
- User ID: The user ID used to create the passkey.
- Public key: The public key returned with the public key credential. This is required to verify a passkey assertion.
- Creation date and time: Record the date and time of the passkey creation. This is useful to identify the passkey.
- Last used date and time: Records the last date and time when the user used the passkey to sign in. This is useful to determine which passkey the user has used (or not used).
- AAGUID: A unique identifier of the passkey provider.
- Backup Eligibility flag: true if the device is eligible for passkey synchronization. This information helps users identify syncable passkeys and device-bound (not syncable) passkeys on the passkey management page.
Follow more detailed instructions at Server-side passkey registration
Signal if the registration fails
If a passkey registration fails, it may cause confusion to the user. If there's a passkey in the passkey provider and available for the user, but the associated public key isn't stored to the server side, sign-in attempts using the passkey will never succeed and it's hard to troubleshoot. Make sure to let the user know if that is the case.
To prevent such a condition, you can
signal an unknown passkey to the passkey provider using the Signal API.
By calling PublicKeyCredential.signalUnknownCredential()
with an RP ID and a
credential ID, the RP can inform the passkey provider that the specified
credential has been removed or does not exist. It's up to the passkey provider
how to deal with this signal, but if supported, the associated passkey is
expected to be removed.
// Detect authentication failure due to lack of the credential
if (response.status === 404) {
// Feature detection
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId: "example.com",
credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
});
} else {
// Encourage the user to delete the passkey from the password manager nevertheless.
...
}
}
To learn more about the Signal API, read Keep passkeys consistent with credentials on your server with the Signal API.
Send a notification to the user
Sending a notification (such as an email) when a passkey is registered helps users detect unauthorized account access. If an attacker creates a passkey without the user's knowledge, the passkey remains available for future abuse, even after the password is changed. The notification alerts the user and helps prevent this.
Checklist
- Verify the user (preferably using email or a secure method) before allowing them to create a passkey.
- Prevent creating duplicate passkeys for the same passkey provider using
excludeCredentials
. - Save the AAGUID to identify the passkey provider and to name the credential for the user.
- Signal if an attempt to register a passkey fails with
PublicKeyCredential.signalUnknownCredential()
. - Send a notification to the user after creating and registering a passkey for their account.
Resources
- Server-side passkey registration
- Apple document: Authenticating a User Through a Web Service
- Google document: Passwordless login with passkeys
Next step: Sign in with a passkey through form autofill.