Passkeys

Passkeys enable websites to authenticate users without the user having to enter any passwords or other secret codes on the site itself. 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 depend on public key cryptography. A passkey is a public/private key pair that's specific to a particular 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. The public key is stored in the website's server. When the user signs in, the authenticator uses the private key to digitally sign a statement about the user's identity, which is called an assertion. The website's server can use the public key to verify the assertion's signature, and sign the user in.

Authenticators

An authenticator generates and securely stores passkeys, and can generate the digital signatures used to sign assertions. Usually, an authenticator also has a means to authenticate users, often with a biometric such as a fingerprint.

An authenticator might be integrated into the device's operating system, like the Touch ID system in Apple devices or the Windows Hello system, or it might be a removable module like a YubiKey, or it might be an app the user installs, like Bitwarden or LastPass.

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 APIs 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, and if so, what form the attestation should take.

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

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

  • 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

Discoverable and non-discoverable credentials

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

  • Discoverable credentials 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.

  • Non-discoverable credentials 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.

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.

Passkeys must always be discoverable credentials, so RPs implementing passkey-based authentication should always set these options.

Note: Technically, the difference between the two credential types is that 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 may not store the signing key itself in the authenticator, but may instead generate the signing key every time it is needed, from an internal seed and the credential ID value. This means that they need the RP to provide the credential ID value for them to generate a signature. One advantage of this is that the keys take up less storage space, but this is not an issue for passkey authenticators.

"Resident key" is an old, deprecated term for "discoverable credential", but the old term is still used in the WebAuthn API for backwards compatibility.

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 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. In attestation, an authenticator provides verifiable evidence to the RP about its origin, and about 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 can 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 authenticate themselves, for example with 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 should 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.

Handling lost passkeys

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

To ensure that such a user is not locked out of their account, a website can encourage users to create multiple passkeys, in different authenticators, for the same account. For example, the user could have one passkey in an authenticator integrated into the device, and another on a removable authenticator, which they could use as a backup in case they lose their device.

Additionally, some authenticators support backup by various methods, such as cloud sync or manual export. The signed assertion returned from a call to get() 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 an authenticator that is backup eligible.

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

Migrating from passwords

[wip]

Security properties of passkeys

[wip]

We can note some features of this design that make it inherently more secure than passwords:

  • The user never has to remember any secret or enter any secret on the site.
  • The server doesn't have to store any secrets: if an attacker steals the user's public key, they can't do anything damaging with it.
  • When the user tries to sign in, the browser will only look for passkeys that are associated with the current site. 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.