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.

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:

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:

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:

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.

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:

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:

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:

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:

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:

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

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:

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:

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