Passkeys

Passkeys enable websites to authenticate users without the user having to enter any passwords or other secret codes on the site itself. They address many of the most serious weaknesses of other authentication methods such as passwords.

They're considered the most secure authentication method available to websites, and we recommend that sites should adopt passkeys as their preferred authentication method, and phase out the use of passwords.

Instead of a shared secret, passkeys rely on public-key cryptography. A passkey is a public/private key pair bound to a specific user's account on a particular website.

The private key is stored in a module called an authenticator, that's in, or attached to, the user's device. An authenticator might be built into the platform, or a separate hardware key like a YubiKey, or a credential manager app like KeePassXC.

The public key is stored on the website's server. When the user signs in, the authenticator uses the private key to digitally sign a challenge value from the server, together with contextual information such as the requesting origin. The resulting object is called an assertion. The website's server can use the public key to verify the assertion's signature, and sign the user in.

In this guide we'll:

The WebAuthn API

To interact with an authenticator, a website uses the Web Authentication API (WebAuthn). In the WebAuthn specification, a website that uses passkeys to authenticate users is called a Relying Party (RP), and we'll use that term in this guide.

WebAuthn is an extension of the Credential Management API, which is a framework for managing credentials for various authentication methods, including passwords and federated identity, as well as passkeys.

The two main functions used by RPs are:

Registration

In this section we'll walk through the flow used to create a new passkey and use it to set up a new user account.

Overview of user registration with passkeys.

When the user asks to register on a site, the RP's front-end code first asks its server for a challenge: this is a random value generated on the server, that the server will later use to ensure that the resulting passkey was generated in response to this request.

Next, the RP's front-end code calls CredentialsContainer.create(). It can specify various options, including:

  • Attestation preferences: Whether the RP is interested in authenticator attestation (a mechanism that helps the RP decide if it should trust the authenticator), and if so, what form the attestation should take.

  • Authenticator preferences: What type of authenticator to use, and whether the authenticator should perform user verification before creating the passkey.

  • Challenge: The challenge generated by the RP's server. This helps protect against replay attacks.

  • Website information: A human readable name and ID for the RP that will be associated with the new passkey. The ID determines the scope of the resulting passkey.

  • User information: Information about the user that will be associated with the new passkey, including a human-readable display name, an account identifier, and a human-readable account identifier such as an email address or username.

Depending on the authenticator capabilities and the preferences of the RP, the authenticator may ask the user to authorize passkey creation via some user verification method: for example, using a biometric such as a fingerprint.

The authenticator then creates a passkey for the account. It stores the private key locally and returns an object containing the public key, challenge, and some additional information. If the authenticator is performing attestation, then this is all digitally signed with either the private key or an attestation key belonging to the authenticator.

The RP's front-end code sends this to the server, which:

  • Verifies the attestation, if attestation is taking place
  • Verifies that the challenge is the expected value
  • Creates a new user account and stores the public key in it along with with the user's account information.

Sign in

In this section we'll walk through the flow used to sign a user in with a passkey.

Overview of user sign-in with passkeys.

When the user tries to sign in, the RP's front-end code again asks the server for a challenge value.

Next, the RP's front-end code calls CredentialsContainer.get(). It can specify various options, including:

  • Allowed credentials: An array of identifiers for the passkeys that the RP will accept. This array may be empty or omitted, in which case any suitable passkeys may be used.

  • Challenge: The challenge generated by the RP's server.

  • Website ID: The ID of the RP which is trying to sign the user in. See Passkey scope.

  • User verification: Whether the authenticator should perform user verification before using the passkey.

Next, the browser finds passkeys matching the given criteria: if it finds more than one, it may ask the user to choose one. The authenticator which stores this passkey will typically ask the user to authorize the use of this passkey, including user verification if this is requested by the RP and supported by the authenticator.

The authenticator will then use the passkey's private key to create a digitally signed assertion, including the challenge and other data.

The RP's front-end sends the assertion to the server, which verifies the signature using the public key it stored. If verification is successful, then the user can be signed in.

Features of WebAuthn

In this section we'll go into some more detail about various aspects of the WebAuthn API.

Platform and roaming authenticators

The WebAuthn API distinguishes two types of authenticators:

Platform authenticators

These authenticators are not removable from the device. For example, authenticators built into the device's operating system, like the Touch ID system in Apple devices or the Windows Hello system.

Roaming authenticators

These authenticators can be removed from the device and attached to a different device. The classic example of this is an authenticator implemented in a USB key, like a YubiKey.

When an RP creates a new passkey it can ask which type of authenticator it wants to use, as part of the authenticatorSelection option that it passes to CredentialsContainer.create().

The main advantage of a platform authenticator is that it's convenient for the user: they don't have to keep track of a separate piece of hardware. The main disadvantage is that it can only be used with its host device.

Platform authenticators can sometimes function as roaming authenticators: for example, a platform authenticator on a mobile device might be available to a laptop as a roaming authenticator, via a Bluetooth connection.

Although platform authenticators can't be removed from their device, they can often share their passkeys with other authenticators via cloud sync or import/export functions. For example, a platform provider might enable users to share their passkeys across all the devices belonging to their product family.

Discoverable and non-discoverable credentials

The WebAuthn specification distinguishes between discoverable and non-discoverable credentials.

  • Discoverable credentials, also known as resident keys, are those that can be used without the RP first needing to identify the user who is being authenticated: that is, the "allowed credentials" array passed into CredentialsContainer.get() can be empty. With a discoverable credential, all the signing key material is stored in the authenticator, so the authenticator is able to generate signatures without needing any input from the RP.

  • Non-discoverable credentials, also known as non-resident keys, are those for which the RP must first identify the user who is being authenticated (for example, by having them enter their username), and then pass the associated credential ID into CredentialsContainer.get(), in the "allowed credentials" array.

    Non-discoverable credentials need the credential ID because they do not store the signing key itself in the authenticator, but instead generate the signing key every time it is needed, from an internal seed and the credential ID value. That is, the account key is not resident in the authenticator.

The advantage of using non-discoverable credentials is that an authenticator with limited storage can support a potentially unlimited number of accounts, because the key material for each account is not stored in the authenticator.

The advantage of using discoverable credentials is that they enable a browser to implement autofill with public key credentials, which makes it much easier for users to sign in, especially when they might have both public key credentials and passwords for a given site.

For this reason, passkeys must always be discoverable credentials, so RPs implementing passkey-based authentication should always make them discoverable.

To create a discoverable credential, the RP should set the residentKey option to "required" and the requireResidentKey option to true when it creates a new credential in the CredentialsContainer.create() call.

Challenges

When an RP asks an authenticator to create a new passkey or to use an existing passkey, it must provide a challenge. This is a random value, specific to the request, that would not be predictable by an attacker. The challenge must be generated in a trusted environment (which generally means, on the server, not the front end).

The RP's front-end code passes the challenge into the create() or get() call, and the browser includes the same value in the object returned by these methods. In the case of get(), the challenge value is also part of the input to the digital signature calculated by the authenticator.

When the web server verifies the response from the authenticator, the web server needs to check that the challenge is the same value it originally provided.

The web server should also invalidate the challenge value after about 10 minutes, and reject any responses containing the challenge that have arrived after this time.

The challenge represents evidence that the authenticator's response was a response to this request, and not an old response to some previous request that an attacker has managed to steal. This kind of attack is known as a replay attack.

Attestation

The security of a passkey depends in part on the reliability of the authenticator used. For example, if an authenticator does not protect the private keys it stores, then an attacker could steal the keys and impersonate users. WebAuthn defines an optional mechanism called attestation, in which an authenticator can provide verifiable evidence to the RP about the authenticator and the data it produces (such as key pairs or signed assertions). This can help the RP decide whether it wants to rely on the authenticator to authenticate its users.

To implement attestation, the authenticator contains a key pair called an attestation key, which was built into the device at manufacturing time, and which is certified as belonging to the organization that made this authenticator. For example, the certificate could state that this authenticator was produced by "Acme Authenticator Incorporated".

When the authenticator creates a new passkey, it signs the resulting object with its attestation key. The RP verifies the signature and the associated certificate, and then has evidence that the passkey was made by an authenticator produced by "Acme Authenticator Incorporated".

Not all authenticators support attestation, and RPs may indicate that they are not interested in attestation. In these situations, the object returned by a call to CredentialsContainer.create() may not be signed at all, or it may be signed using the passkey itself (this is referred to as self attestation). In these situations, the RP has no reliable evidence of the authenticator's origin or capabilities.

User verification

When a website calls CredentialsContainer.create() to create a new passkey, or calls CredentialsContainer.get() to create an assertion, the authenticator will always ask the user to consent to the operation.

The RP can also ask the authenticator to perform user verification, which means the user will be asked to authorize the use of their credential, for example by entering a PIN or a biometric such as a fingerprint.

When this happens, it's considered to be a form of multi-factor authentication: the authenticator itself is "something the user has" while the PIN or biometric are respectively "something they know" or "something they are".

Note that not all authenticators support user verification.

Passkey scope

The scope of a passkey determines which sites will be allowed to use the passkey.

By default:

  • When a page creates a passkey by calling CredentialsContainer.create(), the browser sets the passkey's RP ID to the domain component of the caller's origin, and the authenticator stores this value alongside the passkey.

  • When a page uses a passkey by calling CredentialsContainer.get(), the browser passes the domain component of the caller's origin to the authenticator, and the authenticator will only allow the passkey to be used if this value matches the stored RP ID.

This means that by default, a passkey can only be used by a page from the same origin (excluding the port) as the page that originally created it.

Websites are allowed to relax these rules, within some constraints:

  • When a website creates a passkey, it can pass an ID into CredentialsContainer.create(), and the authenticator will use this as the RP ID.

  • Similarly, when a website tries to use a passkey, it can pass an ID into CredentialsContainer.get(), and the authenticator will compare this ID with the stored RP ID.

For both create() and get(), the passed value must be a registrable domain that is a domain suffix of the domain of the caller's origin.

This relaxation means that, for example, a page at https://register.example.com may create a passkey with an RP ID of example.com, and a page at https://login.example.com will then be allowed to use that passkey.

Passkey scope helps to defend against phishing attacks. In a phishing attack, the user is presented with a malicious page that looks like the target site, and that asks the user to enter their credentials for the target site. Typically, the URL of the malicious site appears similar to that of the target site, helping to confuse the user. For example, if the target site is https://example.com, then the phishing site might be served from https://examp1e.com.

With the scope rules for passkeys, though, a site served from https://examp1e.com is not able to use passkeys that were created for https://example.com.

Origin verification

The signed assertion returned by an authenticator includes information about the context of the caller:

  • The origin of the document that called CredentialsContainer.get().
  • If the caller was embedded as an <iframe>, whether the caller had the same origin as the top-level document.
  • The origin of the top-level document, if the caller was embedded as an <iframe> and was not same-origin with the caller.

When the RP server verifies the assertion, it must check that these values are what it expects to see.

This provides a layer of protection against phishing attacks, in addition to that provided by passkey scope.

Security properties of passkeys

Passkeys are more secure than passwords, and we can see how their design addresses the most serious weaknesses of passwords:

  • Unlike a password, the user never invents a passkey value or needs to remember it. This means users can't choose weak passkey values, so they are not vulnerable to guessing attacks. Passkey generation is transferred from the user to the authenticator.

  • Passkeys are never reused across sites, so they are not vulnerable to credential stuffing attacks. If an attacker does get access to a passkey, they can only use it for the site that originally created it.

  • With passkeys, the server never has to store any secrets: it just stores the public key. So if an attacker breaks into the server's database, they can't compromise the private key, which is stored in the authenticator. Note, however, that they can compromise user accounts if they can write fake credentials into the server's database.

  • When the user tries to sign in, the browser will only look for passkeys whose scope matches the requesting site, and the RP's server can verify that the origin of the requester was what they expected. This makes passkeys resistant to phishing attacks, because front-end code served from a phishing site like https://examp1e.com is not able to use the passkey associated with https://example.com.

Although passkeys provide protection against these common web authentication attacks, they don't eliminate all threats. Since widespread deployment of passkeys is relatively new, there isn't yet a mature understanding of the attacks that passkeys may face, but it's likely that some attacks would focus on the user's devices: for example, convincing them to install a malicious authenticator. Attacks may also target parts of the authentication system that are not secured by passkeys, such as account recovery mechanisms.

Handling lost passkeys

If a user loses an authenticator, whether it's a separate module or integrated into their phone, they lose all the passkeys it contains.

In this section we'll discuss two strategies for dealing with authenticator loss:

Creating multiple passkeys

In contrast to the advice regarding passwords, RPs are encouraged to create multiple passkeys for a single account. A common pattern would be to have:

The excludeCredentials option, passed to CredentialsContainer.create(), lists credential IDs, and tells the browser that the authenticators containing the listed keys must not be used for the new key. That is, it is a way for the RP to ensure that the new passkey is created in a new authenticator.

Passkey backup

Some authenticators support backup by various methods, such as cloud sync or manual export. The signed assertion returned from a call to get() includes a set of flags, which, among other things, indicates whether the passkey:

  • Is backup eligible: that is, whether it is stored in an authenticator that supports backup
  • Has in fact been backed up.

An RP can use this information to help a user manage their credentials. For example:

  • If the passkey is not backup eligible, then the RP might respond by inviting the user to create another passkey in another authenticator that could be used as a backup.

  • If the RP is migrating users away from passwords, and the user has an old password as well as a passkey, and the assertion indicates that the passkey has been backed up, then the RP might invite the user to delete their old password, since they don't need it as a backup any more.

Managing passkeys

We've seen that a user may have multiple passkeys for a single account, distributed across multiple authenticators and multiple devices. Each passkey corresponds to a WebAuthn credential, with private key material protected by the authenticator and a corresponding public key stored by the RP as part of the user's account information.

Sometimes the user might need to delete a passkey for their RP account: this essentially means deleting the public key stored in the RP's server, so that the corresponding private key can't be used to sign the user in anymore. This is generally needed when the user doesn't have control of the authenticator anymore, for example, because they have lost the device containing it.

This means that an RP should implement a means for an authenticated user to view the registered passkeys for their account and delete specific public keys. For each key, the RP should display information to help a user understand which key it is and which authenticator it is associated with. This can include:

Additionally, the user should be able to edit the passkey name and delete the passkey.

If the user tries to delete the last passkey, the RP should inform them of the implications of this: the RP might allow the user to sign in with a different method such as a one-time code, or they might not be able to access their account any more.

See also Help users manage passkeys effectively.

Synchronizing server and authenticators

Note that if the user deletes a passkey on the RP's server, this introduces an asymmetry between the server and the authenticator that contains the corresponding private key. The authenticator still thinks the passkey is valid, so the browser may offer it to the user as a sign-in option, but the RP will no longer accept its assertions.

To reduce the chance of problems like this, the WebAuthn API defines a set of static methods of PublicKeyCredential that enable an RP to tell authenticators about server-side changes:

  • PublicKeyCredential.signalUnknownCredential() tells the browser that a specific passkey was not recognized by the RP, and is typically called by the RP immediately after the user tried to sign in with this passkey. The most common scenario here is that the user deleted this passkey on the server, and then mistakenly tried to sign in with it.

  • PublicKeyCredential.signalAllAcceptedCredentials() gives the browser the identifiers of all the passkeys that the RP currently accepts as valid, to enable all attached authenticators to update their stored keys. It could be called every time the user successfully authenticates. This API must only be called for authenticated users, because it exposes the user's credential IDs.

  • PublicKeyCredential.signalCurrentUserDetails() tells the browser the user's current username and display name, and should be called when an authenticated user changes these values. This API must only be called for authenticated users, because it exposes user data.

Migrating from passwords

Most websites that add passkey support will already support password-based authentication, and will have an existing base of users with passwords. These users are not safe from the weaknesses of passwords until they not only have and use passkeys on your site, but no longer even possess passwords associated with their accounts.

You can implement a three-step process to migrating users from passwords:

Creating passkeys alongside passwords

The first step here is to offer users the opportunity to create a passkey when they successfully sign into your site with a password.

Conditional create

An additional step towards increasing passkey adoption is a feature called conditional create. This allows an RP to create a new passkey for a user's account without requiring any user interaction, when certain conditions are met.

To enable conditional create, the RP calls CredentialsContainer.create(), passing the mediation option set to "conditional":

js
try {
  const publicKeyCredential = await navigator.credentials.create({
    publicKey: options,
    mediation: "conditional",
  });
  // handle new passkey creation
  // let the user know that they have a passkey now
} catch (e) {
  // passkey was not created
}

With this option:

  • If the user has just signed in with a password, using a password manager that also supports passkeys (that is, a credentials manager that can also function as an authenticator), then the browser will ask that credentials manager to create a new passkey for the user, without asking the user.

  • Otherwise, the create() call will fail.

From the user's point of view, if the create failed, they don't know it was made, and if it succeeds, the RP can inform them that they have a passkey that they can use to sign in next time.

The theory here is that if the user is relying on a credentials manager for sign-in already, then they implicitly trust it to look after their sign-in credentials in general, so they can trust it to create a new form of credential for them.

Using passkeys alongside passwords

If a user has both a password and one or more passkeys, they can choose to use either to sign in, and the RP might want to encourage them to use the passkey.

In the transitional period, a user could have either passwords or passkeys for their account, or both. In this situation, a UI that asks them which method they want to sign in with can be confusing: they might not remember which method they have for which account.

Autofill UI

One technique to help users in this situation is the autofill UI, also sometimes called conditional mediation.

In this technique, the RP's sign-in page offers the user a form, which allows them to sign in with a username and password. In the field for the username, the RP adds an autocomplete value of "webauthn":

html
<input type="text" name="username" autocomplete="username webauthn" autofocus />

In the background, the RP starts the normal process to request an assertion signed with a passkey: it fetches a challenge from the server and prepares the other options to CredentialsContainer.get().

However, when the RP calls get(), it passes the mediation: "conditional" option (just like with conditional create):

js
const assertion = await navigator.credentials.get({
  publicKey: options,
  mediation: "conditional",
});

The effect of this is that the call waits until the user interacts with the username field. When the user interacts with the field, the browser looks for passkeys that can be used to sign into the RP, and displays them to the user as autofill values. If the user selects one, then the selected passkey is used, and the RP can use the resulting assertion to sign the user in.

If the user doesn't have a passkey for the site, or they don't select one of the offered passkeys, then they can enter their username and password, or have it autofilled by their password manager.

This means that you can support users who may have passwords or passkeys, or both, without any special UI, and without the user having to remember whether they actually do have a passkey for your site.

Retiring passwords

Even if a user has a passkey for your site, and uses it in preference to their password, they are still vulnerable to attacks such as credential stuffing, guessing, and phishing for as long as you retain a password for their account.

So as a final step, you might want a user to delete their password entirely. You can offer this as an option in their account settings, and potentially nudge them to delete their password if they haven't used it in a long time (but have used their passkeys regularly).

However, you should also consider that having a password helps protect a user against being locked out of their account if they lose access to their passkey. Before encouraging users to delete their password, you can check that they have alternative protection, such as multiple passkeys on different authenticators, and/or passkeys that have been backed up.

See also