The developer guide about WebAuthn & implementing passkeys. Download the cheat sheet as a PDF or use this website for all information in one place.
Lukas R.
Created: March 6, 2024
Updated: February 17, 2025
Download the Cheat Sheet as a
PDF or
continue reading.
Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.
Authentication with passkeys is based on the two processes, also called
ceremonies, registration(aka the attestation phase) andlogin
(aka the assertion phase).
Each phase requires a random challenge generated by the server, which is
signed by the authenticator and sent back to the WebAuthn server to verify the user.
The registration
ceremony uses two central objects: PublicKeyCredentialCreationOptions and
attestation.
The login
ceremony uses two central objects: PublicKeyCredentialRequestOptions and
assertion.
For the registration and login with passkeys, there are four main objects:
This section also explains the authenticatorSelection - object, which is used in the PublicKeyCredentialCreationOptions.
Ben Gould
Head of Engineering
I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.
10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.
Join Passkeys CommunityPublicKeyCredentialCreationOptions is the central object of the attestation phase (Registration). It is created by and returned from the WebAuthn server.
{ "PublicKeyCredentialCreationOptions": { "rp": { "id": "passkeys.eu", "name": "Corbado Passkeys Demo" }, "user": { "displayName": "john.doe", "id": "dXNyLZ….DU10Tc", "name": "john@doe.com" }, "challenge": "888fix4Bus...pHHr3Y", "pubKeyCredParams": [ { "alg": -7, "type": "public-key" }, { "alg": -257, "type": "public-key" } ], "excludeCredentials": [], "authenticatorSelection": { "authenticatorAttachment": "platform", "residentKey": "required", "userVerification": "required", "attestation": "none", "extensions": [] } } }
The object contains these attributes:
PublicKeyCredentialRequestOptions is the central object of the assertion phase (Login). It is created by and returned from the WebAuthn server.
{ "publicKeyCredentialRequestOptions": { "challenge": "pT7HMA-…dFPHk", "timeout": 500, "rpId": "passkeys.eu", "userVerification": "preferred", "allowCredentials": [], "extensions": [] } }
The object contains these attributes:
During the Attestation / Registration Ceremony, the Authenticator returns this Registration Response. You can try this yourself in the Passkeys Debugger.
{ "authenticatorAttachment": "platform", "id": "JKZbixUfKN_aZtimefYT-OjH5dw", "rawId": "JKZbixUfKN_aZtimefYT-OjH5dw", "response": { "attestationObject": { "fmt": "none", "attStmt": {}, "authData": { "rpIdHash": "PpZrl-Wqt-OFfBpyy2SraN1m7LT0GZORwGA7-6ujYkM", "flags": { "userPresent": true, "userVerified": true, "backupEligible": true, "backupStatus": true, "attestedData": true, "extensionData": false }, "counter": 0, "aaguid": { "raw": "fbfc3007-154e-4ecc-8c0b-6e020557d7bd", "name": "iCloud Keychain" }, "credentialID": "JKZbixUfKN_aZtimefYT-OjH5dw", "credentialPublicKey": "pQECAyYgASFYIPWLalDzyxIDmAADvfK8iNM5To50kh7TyPH-teEz8RMdIlgg3D7bPIWQJ8z-WFn3zdYZzJw9c7mhPdmflQqD9vV7efA", "parsedCredentialPublicKey": { "keyType": "EC2 (2)", "algorithm": "ES256 (-7)", "curve": 1, "x": "9YtqUPPLEgOYAAO98ryI0zlOjnSSHtPI8f614TPxEx0", "y": "3D7bPIWQJ8z-WFn3zdYZzJw9c7mhPdmflQqD9vV7efA" } } }, "clientDataJSON": { "type": "webauthn.create", "challenge": "k2p6f-upzP_hc6NZvmMAxiI0VSTeQIeXXVRGW62LTj0", "origin": "https://www.passkeys-debugger.io", "crossOrigin": false }, "transports": [ "hybrid", "internal" ], "authenticatorData": "PpZrl-Wqt-OFfBpyy2SraN1m7LT0GZORwGA7-6ujYkNdAAAAAPv8MAcVTk7MjAtuAgVX170AFCSmW4sVHyjf2mbYpnn2E_jox-XcpQECAyYgASFYIPWLalDzyxIDmAADvfK8iNM5To50kh7TyPH-teEz8RMdIlgg3D7bPIWQJ8z-WFn3zdYZzJw9c7mhPdmflQqD9vV7efA", "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9YtqUPPLEgOYAAO98ryI0zlOjnSSHtPI8f614TPxEx3cPts8hZAnzP5YWffN1hnMnD1zuaE92Z-VCoP29Xt58A", "publicKeyAlgorithm": -7 }, "type": "public-key", "clientExtensionResults": {} }
The attestation contains some important components like attestationObject, algorithm and the transport-flags.
Taken from W3C's Webauthn specification
The attestationObject is a CBOR-encoded object, containing information about the newly created credentials, the public key and other relevant data:
Read more about the extensions.
Passkeys are generated with COSE algorithms, indicating the used algorithm
in the algorithm-attibute of parsedCredentialPublicKey in the attestation object.
Here's an overview of the most relevant COSE algorithms:
The transports -property indicates mechanisms through which an authenticator can communicate with a client. Some common, sample value combinations are:
During the Assertion / Login Ceremony, the Authenticator returns this Login Response. You can try this yourself in the Passkeys Debugger.
{ "id": "JKZbixUfKN_aZtimefYT-OjH5dw", "rawId": "JKZbixUfKN_aZtimefYT-OjH5dw", "type": "public-key", "authenticatorAttachment": "platform", "response": { "authenticatorData": { "rpIdHash": "PpZrl-Wqt-OFfBpyy2SraN1m7LT0GZORwGA7-6ujYkM", "flags": { "userPresent": true, "userVerified": true, "backupEligible": true, "backupStatus": true, "attestedData": false, "extensionData": false }, "counter": 0 }, "clientDataJSON": { "type": "webauthn.get", "challenge": "GCVkITWbe2l2dttsn_DgJYvH9QPHPDo0ygWgcgI6B7U", "origin": "https://www.passkeys-debugger.io", "crossOrigin": false, "other_keys_can_be_added_here": "do not compare clientDataJSON against a template. See https://goo.gl/yabPex" }, "signature": "MEQCIA-orC8N2KKWOxyY17BWP8lB-Be5to9btXRnJZf2SLhXAiBGxJe5Eu5LwOTbsyzAYmIXHOhlC3pN7s7Q1fRLvEW57g", "userHandle": "_FKz1uwqmR_3yGq6hJntzoIFwFC_d1u_53YRELh0KlE" } }
The assertion contains some important components like flags, signature and userHandle.
Here's an overview of the most relevant flags and their combinations:
The signature is used to verify that the user trying to log in, actually has the private key. The signature is created by concatenating the authenticatorData and clientDataHash (i.e. the SHA-256 version of ClientDataJSON) and signing the result with the private key (in the authenticator). To verify with the public key, we concatenate authenticatorData and clientDataHash as well. If the verification result returns true, the authentication is successful.
The userHandle is the actual user_id. Read more about the user_id in section 4.1 Database Schema.
The authenticatorSelection - object allows the server to dictate¨settings for the authenticator and credential creation with the following values:
Resident Keys (also called Discoverable Credential): Resident keys are stored on the authenticator and retrieved during authentication. ¨This way the client can discover a list of possible keys, which is why [Conditional UI](/blog/user-transition-passkeys-> conditional-ui) requires resident keys. Non-Resident Keys (also called Non-Discoverable Credential): In case of non-resident keys, the credential ID is stored on the server and not on the authenticator. During each authentication, the authenticator derives the private key from a seed within the credential ID and an internal master key that is saved on the authenticator.
Warning: If set to "Preferred", the user or his device can skip the user verification in the authentication process (read more in this article).
Conditional UI (passkey autofill) displays available passkeys in a selection dropdown for the user, when a user has a resident key registered with the relying party. ¨It improves the usability of passkeys, but requires additional development efforts and is not available for all OS / browser combinations.
Like a regular login, Conditional UI also uses the objects
PublicKeyCredentialRequestOptions and
assertion
Conditional UI is not available on all combinations of operating systems and browsers (yet). Here's an overview of the current browser coverage (March 2024):
For an up-to-date overview see this website.
A full, minimalistic code for a Conditional UI method looks like this:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Conditional UI</title> </head> <body> <input type="text" id="username" autoComplete="username webauthn" /> <script> async function passkeyLogin() { try { // retrieve the request options (incl. the challenge) from the WebAuthn server let options = await WebAuthnClient.getPublicKeyRequestOptions(); const credential = await navigator.credentials.get({ publicKey: options.publicKeyCredentialRequestOptions, mediation: "conditional", }); const userData = await WebAuthnClient.sendSignedChallenge(credential); window.location.href='/logged-in'; } catch (error) { console.log(error); } }; passkeyLogin(); </script> </body> </html>
Conditional UI only works with resident keys / discoverable credentials
Its recommended to provide a different server endpoint to start the
Conditional UI login.
The client needs to meet multiple requirements:
To avoid errors, the server should first test the clients availability with this function:
// source: https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/isConditionalMediationAvailable#examples // Availability of `window.PublicKeyCredential` means WebAuthn is usable. if ( window.PublicKeyCredential && PublicKeyCredential.isConditionalMediationAvailable ) { // Check if conditional mediation is available. const isCMA = await PublicKeyCredential.isConditionalMediationAvailable(); if (isCMA) { // Call WebAuthn authentication start endpoint let options = await WebAuthnClient.getPublicKeyRequestOptions(); const credential = await navigator.credentials.get({ publicKey: options.publicKeyCredentialRequestOptions, mediation: "conditional", }); /* ... */ } }
The input field should receive an HTML autofill token, that signals the client to populate passkeys to the ongoing request. Besides passkeys, the autofill tokens can be paired with existing tokens, e.g. usernames and passwords:
<label for="name">Username:</label> <input type="text" name="name" autocomplete="username webauthn"> <label for="password">Password:</label> <input type="password" name="password" autocomplete="current-password webauthn">
There is no mandatory or standardized database schema for WebAuthn servers. ¨However, this example database schema can be used to store the required information and provide all functionalities of a WebAuthn server:
Bold attributes are mandatory for a minimal viable implementation, while the others are only needed for optional, but helpful features.
User DisplayName (user.displayName): User-friendly, readable name that is typically the full name of the user. ¨Its shown to the user, but not used during authentication.
User Name (user.name): Unique and readable name that is typically an e-mail address or a username. It can be shown to the user, but it's not used during authentication.
The Relying Party ID (rpID) is a domain stored within the passkey, ensuring the passkey only works for the correct domain (browser URL, see this article for native apps). During authentication, the rpID is checked against the browser URL and only allowed in these two cases:
Here are examples for (dis-)allowed combinations:
Here's a list of useful tools & websites for implementing passkeys.
chrome://device-log
)If you want to implement passkeys with just a few lines of code into any application, you can also use Corbado Complete (for new apps) or Corbado Connect (for existing apps)
Table of Contents
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Passkey Tutorial: How to Implement Passkeys in Web Apps
Vincent - December 7, 2023
WebAuthn Resident Key: Discoverable Credentials as Passkeys
Vincent - September 28, 2023
WebAuthn Relying Party ID (rpID) & Passkeys: Domains & Native Apps
Vincent - September 21, 2023