Passkeys – you may have heard of them, or perhaps you have no idea what they are. Let me introduce them to you, how they can significantly simplify user authentication, and at the same time improve the security of your web application's authentication process. We'll explore how it all works, from in and out. A fully functional demo with complete source code awaits you. We'll also take a look at where passkeys are headed and what new features they will soon offer. I'll complement this with my own experiences to highlight the essentials. A good cup of coffee will definitely come in handy, so let's dive in!
Table of Contents
- How Many of Your Passwords Have Been Compromised?
- So What Are Passkeys?
- Security Risks & User Privacy
- Is It Worth It?
How Many of Your Passwords Have Been Compromised?
Let’s at least approximate the answer. According to the service haveibeenpwnded.com, my email address appears in 24 data breaches. In many cases, along with passwords and other sensitive data. We can be upset about the imperfect implementation of the given service, but the world of security is ever-changing, and what was considered safe a few years ago might now serve as a false sense of security. It’s a world full of probabilities and calculated risks, where we rely on the assumption that even the most powerful computer in a few years won’t be able to break through the security layer of our application.
Feel free to check how long it would take to crack a password similar to yours tools by password managers like Bitwarden or NordPass. While these estimates should be taken with a grain of salt, they can give you a rough idea of your password’s strength.
The problem is that even if you choose a password so secure that it would take longer to crack than for the Sun to become extinguished, you can’t rule out the possibility of an attacker finding another way to access your account. For example, you might fall victim to a phishing attack and inadvertently give your password to the attacker yourself. Thinking that you’re logging into a familiar application, you enter your credentials — only to notice the slightly different URL when it’s already too late.
No matter how an attacker might obtain your password, it’s better to design your security layer so that even a leaked password won’t grant access to your account. How? Through multi-factor authentication (MFA), as suggests OWASP:
MFA (Multi-Factor Authentication) is a verification proces that requires the user to provide at least two factors, which can be divided into three main categories:
- Something you know: password, PIN, security question, etc.
- Something you have:
- smartphone:
- to receive a one-time code via SMS (OTP – One-Time Password)
- to generate a time-based one-time code using an app like Google Authenticator (TOTP – Time-Based One-Time Password)
- USB security key
- etc.
- smartphone:
- Something you are: biometric authentication (fingerprint, facial recognition)
In the quotation above OWASP specifically mentions FIDO2 passkeys as a potential MFA solution that is more user-friendly than current methods.
So What Are Passkeys?
Simply put, passkeys are an easier and more secure way to authenticate users. Instead of using a password, you can rely on methods like a fingerprint or facial recognition, depending on the type of authenticator supported by your device.
After all, try it out for yourself! Here’s a fully functional demo where you can create a passkey and then use it for login:
🖼️ Demo: with-webauthn.dev
👨💻 Source code: github.com/cermakjiri/with-webauthn
No matter which type of authenticator you chose during the demo, the following steps happened behind the scenes, simplified:
- The authenticator generates a cryptographically linked pair of keys
- The private key signs a challenge generated by your server (known as the relying party).
- The private key remains stored in the authenticator (this applies only to device-bound passkeys, which will be discussed in detail later).
- The authenticator returns a result that includes, among other things, the public key and the signature — referred to as a public-key credential or, more commonly, a passkey.
- The result from the authenticator is sent to your server (the relying party), where the public key is used to verify the signature.
You may also encounter the term FIDO2 passkeys. This is simply a more formal way of referring to the same concept. FIDO2 (Fast IDentity Online 2) is a collaborative project between the FIDO Alliance and the W3C (World Wide Web Consortium), aimed at creating a new set of secure standards for user identity verification on the web. Instead of user-created passwords, the authenticator generates passkeys for authentication.
FIDO2 has two main components:
- Client to Authenticator Protocol 2 (CTAP2) – A communication protocol between the cryptographic authenticator (such as FaceID, TouchID, or a hardware security key) and the client (e.g., a user agent).
- W3C Web Authentication (WebAuthn) – A browser API used to initiate the creation and use of a passkey.
WebAuthn API currently offers “only” two methods:
navigator.credentials.create(...)
: Creates a passkey during the so-called attestation ceremony.navigator.credentials.get(...)
: Retrieves a passkey during the so-called assertion ceremony.
The combination of parameters they accept is truly extensive. You can find the W3C WebAuthn specification here, while MDN or Google Developers provide a more accessible guide.
How to Create a New Passkey?
By calling the navigator.credentials.create(...)
method, a new passkey will be created through a process known as the attestation ceremony:
A more detailed process for creating a passkey:
The client submits an API request to the relying party with the username.
The server generates a
challenge
along with other required parameters:pubKeyCredParams
(COSEAlgorithmIdentifier
)- A list of asymmetric algorithms for signature creation supported by your server (relying party). Not every authenticator supports all asymmetric algorithms, so it’s advisable to include as many as possible. The complete list can be found here.💡If you request an algorithm that the authenticator does not support, you will receive this error, which can be difficult to troubleshoot:
DOMException: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.
- A list of asymmetric algorithms for signature creation supported by your server (relying party). Not every authenticator supports all asymmetric algorithms, so it’s advisable to include as many as possible. The complete list can be found here.
rp
(relying party): The domain where the passkey can be used.user
:id
: Base64 URL encodingname
: username, e.g. emaildisplayName
: full user name
challenge
: Base64 URL encoding
The server also generates a session ID (e.g., as a server-side cookie). Under this ID, it stores the generated challenge and other data in the database that will be needed to verify the authenticator’s response.
The client converts the Base64 URL values to a TypedArray and passes the entire structure to
navigator.credentials.create
:
- The client extracts the public key, transport types, the signature algorithm used, and other details from the
AuthenticatorAttestationResponse
. All this information is then sent to the server for verification. - The server retrieves the expected challenge and other stored data from the database using the session ID. It checks whether the session hasn’t expired yet and validates the AuthenticatorAttestationResponse.
From experience, I can say that parsing the AuthenticatorAttestationResponse
is far from straightforward. For instance, you need to handle decoding the CBOR format — a binary data representation somewhat similar to JSON but designed for communication with authenticators — and then meet all the conditions for proper validation. Therefore, it might be more practical to use a trusted library that handles this for you, such as SimpleWebAuthn.
In the demo, I’m using SimpleWebAuthn on both client and server sides.
However, there are many alternatives available, including libraries in other languages: awesome-webauthn.
Key Points for Creating a Passkey:
Passkeys Are Bound to Your Application’s Domain
- Passkeys are bound to your application’s domain (specified by
rpId
): If the domain changes, the passkeys will become invalid:- A passkey created with
rpId: 'example.com'
can be used on bothhttps://example.com
andhttps://subdomain.example.com
. - A passkey created with
rpId: 'subdomain.example.com'
can only be used onhttps://subdomain.example.com
, not onhttps://example.com
.
- A passkey created with
- For cross-domain support, consider the experimental related origins feature (see the Browsers Support section).
- Passkeys are bound to your application’s domain (specified by
Username Considerations
The username doesn’t have to be an email address, but it can be a practical choice. If you use multiple authentication providers, an email can help unify accounts. When combined with autocomplete="email" and services like iCloud+ (which creates anonymous email aliases), the process becomes simpler and more secure for users. This ensures that only the alias is exposed in case of a data breach, not the user’s actual email address.
Prevent Passkeys Duplicates
If we want to prevent the creation of multiple passkeys for the same service on a single authenticator for the same user (which is likely desirable), we need to populate the excludeCredentials field with the existing passkeys stored in the database.
- Essential for preventing replay attacks. To be effective, the challenge must be regenerated for each use and contain sufficient entropy: NIST recommends a minimum length of 64 bits (Updated in 2020). SimpleWebAuthn generates challenges with a length of 256 bits.
- For security reasons, it is crucial that your relying party server temporarily stores the generated challenge until the registration process is complete and uses the correct challenge during verification (just as during the use of a passkey, i.e., in the assertion ceremony). SimpleWebAuthn documentation suggests a potential solution, a similar solution is implemented in the demo, including OWASP’s security recommendations for configuring server-side cookies
How to Use an Existing Passkey?
By calling the navigator.credentials.get(...)
method, we can retrieve existing passkeys through a process known as the assertion ceremony. Usually:
- User login into the application.
- User verification within the application (e.g., before performing a sensitive action).
- Login using passkey autofill, which streamlines the authentication process.
User Login into the Application:
It does not require a username or a list of existing passkeys. All that is needed is a
challenge
generated by your relying party, and the browser will present a list of all passkeys for the current domain—these passkeys are referred to as discoverable credentials (formerly known as resident keys):The Process of Using a Passkey:
- The server generates a
challenge
:
Similarly to passkey creation, the server generates a session ID (e.g., as a server-side cookie). Under this ID, it stores the generated
challenge
and other data in the database that will be needed to verify the authenticator’s response.The client converts the
challenge
to aTypedArray
and calls theget
method:
- The user is presented with a list of all available passkeys for the given service:
- The server generates a
Verification of a Logged-In User: Before performing a sensitive action, specify the passkey under which the user is already logged in, requiring the user to re-authenticate:
The server returns not only the
challenge
but also the specific passkey (allowCredentials
) — i.e., the ID and the transport types that can be used to connect to the given authenticator:Again, the server generates a session ID (e.g., as a server-side cookie). Under this ID, it stores the generated
challenge
and other data in the database that will be needed to verify the authenticator’s response.Since this is a sensitive action, we set
userVerification: 'required'
:Even more important is to verify the
assertionResponse
to ensure that the authenticator actually performed user verification. See the chapter What is User Presence & User Verification?The user is directly prompted to verify with the specified passkey:
Passkey Autofill: Also known as conditional mediation (or conditional UI), this is a way to improve the UX of logging in with passkeys. Similar to the first example of using a passkey, the user is presented with all available (discoverable) passkeys, but in this case, within select input options:
The server doesn’t respond with any
allowCredentials
(i.e. to get list discoverable credentials):As always, the server generates a session ID.
The client must set the
mediation
argument:At the same time, the corresponding HTML element must have the
autocomplete="webauthn ..."
attribute set:
Key Points for Using Passkeys:
In the context of passkeys autofill:
The
navigator.credentials.get(...)
call must be made as soon as possible after the page loads—before the user clicks on the relevant input field.The get method returns a
Promise
that resolves only when the user selects one of the available passkeys. This also means that if the user doesn’t select a passkey and instead clicks the login button, the application will callnavigator.credentials.get(...)
again, resulting in an error:OperationError: A request is already pending
. To handle this properly, you need to cancel the previous request first using the AbortController API:
userHandle
inassertionResponse
corresponds to the user ID generated during the attestation ceremony (when creating the passkey) – see MDN docs. It can be useful for simplifying the login process. While the user ID is nullable, it must always be defined for discoverable credentials.
Types of Authenticators
Authenticators are categorized primarily based on:
- Communication Type: The method used to connect to the user’s device.
- Storage Capability: Whether the authenticator allows storage of the private key for the created passkey.
Classification of Authenticators by Communication Type
The current version of the WebAuthn API supports communication with various cryptographic authenticators. These are categorized by communication method as follows:
- Platform Authenticators: Embedded within the device, such as biometric sensors, PIN, or pattern recognition.
- Roaming (Cross-Platform) Authenticators: Connectable via USB, Bluetooth, NFC, or a combination of these methods.
WebAuthn API:
A platform authenticator:
A roaming (cross-platform) authenticator:
Without a specified type:
The specific UI may vary depending on the operating system and browser. Here are examples from MacOS and Brave (chromium based) browser.
Classification of Authenticators by Storage Capability
Non-Discoverable Credentials:
Older versions of authenticators did not support storing the private part of the passkey. Instead, such an authenticator encrypted (wrapped) the private part of the passkey, and the resulting ciphertext served as the passkey ID.
During login, the username was always required. The relying party would use it to retrieve a list of all the user’s existing passkeys from the database and send them in allowCredentials
during authentication. The authenticator would then use its private key to decrypt (unwrap) the passkey ID, to retrieve the original private part of the passkey, and proceed with authentication.
This type of passkey was previously referred to as non-resident keys and is now called non-discoverable credentials (officially also known as server-side credentials).
Discoverable Credentials:
In contrast, most modern authenticators now support storing the private part of the passkey directly on the authenticator itself. During login, there is no need to provide a list of the user’s passkeys (allowCredentials
can be empty), thus even the username is not required. The authenticator presents all available passkeys, which the browser displays for the user to select.
These passkeys were previously referred to as resident keys and are now called discoverable credentials.
To create discoverable passkeys, set residentKey: "required"
during passkey creation:
await navigator.credentials.create({
publicKey: {
// ...
authenticatorSelection: {
// 'required' | 'preferred' | 'discouraged' (optional)
residentKey: 'required',
},
},
});
'required'
: The authenticator must create a discoverable credential. If it does not support this, an error will be thrown.'preferred'
: A discoverable credential will be created only if the authenticator supports it; otherwise, a non-discoverable credential will be created.'discouraged'
: The reverse of‘preferred'
.
From here on, I will focus exclusively on discoverable credentials, as I consider them more user-friendly: no need for a username during login and potentially more secure (see Security Risks & User Privacy).
The most basic form of user authentication is called user presence (UP flag), where the user simply presses a button on their authenticator, such as a USB security key, to confirm their presence.
In contrast, user verification (UV flag) requires the authenticator to verify the user through its available methods, such as a PIN, fingerprint, or gesture. This allows the server (relying party) to determine that it is the same user without knowing the user’s exact identity.
The authenticator encodes the results of UP and UV, along with other boolean values (flags), into the Authenticator Data.
The server (relying party) then checks these flags to determine whether the user was authenticated or not: see the complete list of verification conditions for the assertion ceremony.
From the client’s (relying party’s) perspective, i.e., the parameters passed to the get
and create
methods, the level of user verification required can only be expressed as a preference:
userVerification: UserVerificationRequirement
required
: The relying party demands UV, and the operation will fail if the user cannot verify.preferred
(default): The relying party prefers UV but will not fail the operation if it is unavailable.discourage
: The relying party prefers to skip UV.
For a deeper dive, see web.dev – userVerification deep dive or the official WebAuthn specification.
How Many Factors Does Passkey Authentication Use?
All WebAuthn authenticators fulfill the something you have category. Additionally, if they support user verification, they qualify as multi-factor authenticators:
- Authenticators requiring a PIN meet the something you know factor.
- Biometric authenticators fulfill the something you are factor.
Synchronization & Deletion of Passkeys
At the beginning, I mentioned that the private key remains securely stored in the authenticator as part of the passkey creation process. However, based on experience with passkeys, this claim does not seem entirely accurate. For instance, as demonstrated in the demo, you may have noticed that when you create a new passkey, you are offered the option to save it to iCloud Keychain or its alternatives on other operating systems and browsers:
In contrast, if you chose to save the passkey directly in the browser, for example:Based on whether the private key can be transferred or backed up, passkeys can be classified as:
Synced Passkey: A public-key credential whose private key can be transferred outside the originating device. If you use Touch ID or Face ID, it will always result in a synced passkey. While this is a compromise between security and usability, it is also a key factor driving the widespread adoption of WebAuthn. In the demo, it appears as:
Device-Bound Passkey: A public-key credential whose private key remains stored on the originating device. This is a more secure way of using WebAuthn compared to a synced passkey, but it cannot be recovered if the authenticator is lost. In the demo, it appears as:
In the official WebAuthn specification, we can found:
WebAuthn 3 introduces a new feature compared to WebAuthn 2: the ability to transfer the private key (or the entire credential public key source) outside the originating device. However, it does not specify how this transfer should occur.
Previously, if you lost the authenticator, all associated passkeys were permanently gone. This could be mitigated by registering multiple authenticators, but creating such backups was not user-friendly. This issue is now addressed with sync-passkeys, which are typically stored in various cloud keychains depending on the platform you use — for example, iCloud Keychain, the Google Password Manager, and allegedly coming soon to Windows Hello.
Is It Secure? Within the Apple ecosystem:
- The private key is transferred to the cloud through an end-to-end encrypted channel.
- Transfer and storage use AES encryption with a 256-bit key length (since iOS 12).
- Storing it in iCloud requires the Apple account to have two-factor authentication enabled.
If you’re not comfortable with storing passkeys in the cloud, you still have the option of using a physical security key, allowing you to retain full control over the storage location.
The process is similar to obtaining the user presence (UP) and user verification (UV) flags, as described earlier in the context of the assertion ceremony. Instead of checking bit 0 (corresponding to UP) and bit 2 (corresponding to UV), we need:
Bit 3 for the Backup Eligibility (BE) Flag
- This value indicates whether the passkey source (i.e., the private key and associated data) can be transferred outside the authenticator where it was generated.
- The transfer type doesn’t have to be limited to cloud storage; it could also include manual import/export, local network transfers, or even peer-to-peer synchronization, as per the specification.
- A transferable passkey is referred to as a multi-device credential, while a non-transferable one is called a single-device credential.
- This information is obtained during the passkey creation process, known as the attestation ceremony.
Bit 4 for the Backup State (BS) Flag:
- This value indicates whether the passkey source has already been transferred.
Complete parsing of authenticator data can be handled by the previously mentioned SimpleWebAuthn. The client simply sends the authenticator’s response to the server, where it calls:
For enthusiasts of bit manipulation, the CBOR format, and other low-level concepts, I’m including the function from this library for parsing authenticator data – parseAuthenticatorData.ts.
Can WebAuthn provide cryptographic proof of the authenticator’s origin, and what exactly is and isn’t attestation? How can we determine on which authenticator a passkey was created? 👇
The Authenticator Attestation Globally Unique Identifier (AAGUID) is a 128-bit identifier for the type (i.e., make and model) of an authenticator. It is chosen by the manufacturer and is intended to be unique across all authenticators.
WebAuthn attestation is cryptographic proof that an authenticator genuinely originates from the claimed manufacturer. This proof can be verified through a chain of trust, allowing us to confidently assert whether the signature originates from a certificate chain anchored by a root certificate or not.
By default, the create
method does not use this verification mechanism. Therefore, it is recommended to adjust the attestationType
:
SimpleWebAuthn can handle this process for us (assuming attestationType: 'direct'
is set). Based on the fmt
(attestation statement format), it automatically selects the appropriate root certificate. For advanced use cases, it also offers a SettingsService
class, allowing you to set a custom root certificate or verify the one currently in use. The default root certificates used by SimpleWebAuthn are listed here, which you can compare with the official root certificate from Apple, for example.
The passkey-authenticator-aaguids project provides a collection of all available authenticators (names, logos, etc.), which can be helpful when creating a UI for created passkeys.
Key Insights:
Some platforms implementing synced passkeys do not support attestation. Even if you set
attestationType: 'direct'
, the authenticator will return a result withfmt
set tonull
.This is currently the case for Apple:
And for Android:
Windows Hello currently supports only device-bound passkeys, which means attestation is also supported (as of Windows 11). In the foreseeable future, Microsoft plans to implement synced passkeys, allowing users to choose between the two.
How to Delete a Passkey?
The WebAuthn API offers create
and get
methods, but what about delete
? Currently, no such method is available.
A passkey consists of a public part, stored in the application’s database, and a private part, stored in the cloud, on the authenticator, or other locations. From the perspective of the application, we can delete the public part from the database and limit the available passkeys during login by using the allowCredentials
parameter. Additionally, we can illustrate to the user (depending on the platform, or based on the AAGUID) how to remove the private part, see How to Delete a Passkey on Apple, Windows, and Android.
Will it be better? This gap is being addressed by the WebAuthn Signal API in the new version of the WebAuthn specification.
What is it about? The goal is for relying parties to report information about revoked or simply incorrect passkeys (e.g. when a username changes). This communication will be available through new methods:
Official proposal for passkey deletion:
Deleting multiple passkeys by notifying all accepted passkeys (official proposal):
Changing user details (official proposal):
It’s music of the future, we’ll see how soon. Currently, you can try it out in Chrome Canary, see the official demo.
How is Browser Support?
WebAuthn is a widely adopted technology across platforms. However, it is important to note that Windows Hello currently supports only bound-device passkeys, with synced passkeys reportedly arriving soon, as stated in this blog post.
Stable features:
Feature | Chrome | Edge | Firefox | Safari | Source |
---|---|---|---|---|---|
🟢 Core WebAuthn API | 67 | 18 | 60 | 13 | MDN |
🟢 Authenticator supporting user verification | 67 | 18 | 60 | 13 | MDN |
🟢 Passkeys autofill | 108 | 108 | 119 | 16 | MDN |
Current version | 131 | 130 | 133 | 18 |
Upcoming features:
Feature | Chrome | Edge | Firefox | Safari |
---|---|---|---|---|
🟡 Related origins | 128 | 128 | Unknown stand. | 18 |
⚪ WebAuthn Signal API | Canary | Unknown stand. | No public comments. | Positive stand. |
- For more details, checkout passkeys.dev/device-support.
- You can find platform-specific details at passkeys.dev/docs/reference.
Security Risks & User Privacy
Exposure of Unprotected Accounts
Imagine a situation where we have login via traditional username and password, but some users have passkey authentication set up. An attacker attempts to login with various usernames, and the relying party returns existing passkeys (their IDs and transport methods) in the allowCredentials
argument, or throws an error if no passkeys exist for the account. In both cases, the attacker can detect which accounts are not protected with WebAuthn and thus may be more vulnerable to compromise.
How to Prevent This?
One solution could be to always populate the allowCredentials
with either valid or pseudo-random values deterministically derived from the username. This way, the user experience remains unchanged, and the authenticator will only offer valid passkeys. Another option is to exclusively use discoverable credentials, i.e. set allowCredentials: []
, which also prevents potential de-anonymization through passkey IDs.
Additionally, it may be better not to return specific errors from the server that could give clues to the attacker (of course, this is a balance between UX and security).
Source: w3c.github.io/webauthn
Detection of Existing Accounts
In the case where email is used as the username during user registration, to prevent a situation where an attacker can determine if a given email is associated with a user account, it may be appropriate to first send a one-time code to that email address. This verifies that the user actually owns the email address before proceeding with passkey creation.
Source: w3c.github.io/webauthn
For more information on security and privacy of passkeys, you can refer to the official WebAuthn specification.
Is It Worth It?
Although it might not be immediately clear from this article, I am personally very excited about this technology. After a long time, I see a web technology that can genuinely improve the product for end users and, with proper implementation, significantly enhance security and simplify authentication.
If you still don’t believe that passkeys can improve your product:
- How KAYAK reduced sign in time by 50% and improved security with passkeys
- How Yahoo! JAPAN increased passkeys adoption to 11% and reduced SMS OTP costs
And if you’re already thinking about integrating passkeys into your product, I recommend:
- Designing the user experience of passkeys on Google accounts
- passkeys.dev: An education page by members of W3C.
For developers:
- Passkeys developer guide for relying parties
- TypeScript WebAuthn library: simplewebauthn.dev
- Online WebAuthn debuggers: passkeys-debugger.io, debugger.simplewebauthn.dev
Please note, this is a rapidly evolving technology. Therefore, despite the effort to describe everything as accurately as possible, some information may become outdated soon.
Thank you for your attention, and I’d love to hear your thoughts. If you don’t want to miss my future posts, not just about passkeys and WebAuthn, follow me on LinkedIn (no spam – my activity is proof of that).