How WebAuthn and Passkeys Work, Message by Message
A WebAuthn login is a signed challenge. The server sends options with a random challenge, the browser asks an authenticator (a phone, a laptop's platform authenticator, a password manager or a security key) to sign it together with the origin the browser actually sees, and the server verifies that signature with the public key it stored at registration. There are two ceremonies, registration and authentication, and each is one round trip of JSON between your server and the page. This page shows both with fake but realistic values and ties every field to its section in Web Authentication Level 3, a W3C Recommendation.
For what WebAuthn and FIDO2 are, synced versus device-bound passkeys and who implements them, see the WebAuthn / FIDO2 standard page. This page is the wire-level detail underneath it.
The sequence at a glance
| # | Ceremony | Message | Spec |
|---|---|---|---|
| 1 | Registration | Server → page: PublicKeyCredentialCreationOptions |
§5.4 |
| 2 | Registration | Page → server: PublicKeyCredential with AuthenticatorAttestationResponse |
§5.2.1 |
| 3 | Registration | Server verifies and stores the credential record | §7.1 |
| 4 | Authentication | Server → page: PublicKeyCredentialRequestOptions |
§5.5 |
| 5 | Authentication | Page → server: PublicKeyCredential with AuthenticatorAssertionResponse |
§5.2.2 |
| 6 | Authentication | Server verifies the assertion and starts a session | §7.2 |
Between the page and the authenticator sits CTAP (for roaming authenticators) or a platform API; the relying party never sees that leg. Level 3 adds PublicKeyCredential.parseCreationOptionsFromJSON() and parseRequestOptionsFromJSON() (§5.1.8–5.1.9) and toJSON(), so the base64url JSON shown below is what actually crosses the network.
Message 1: creation options
{
"rp": { "id": "example.com", "name": "Example Corp" },
"user": {
"id": "dS0xMjM0NS1leGFtcGxlLW9wYXF1ZQ",
"name": "ada@example.com",
"displayName": "Ada Example"
},
"challenge": "q2Tb3pJm0w8HcXyO7nV5fA9sLk1eRu4Z",
"pubKeyCredParams": [
{ "type": "public-key", "alg": -7 },
{ "type": "public-key", "alg": -257 }
],
"timeout": 300000,
"excludeCredentials": [
{ "type": "public-key", "id": "b2xkLXNlY3VyaXR5LWtleS1leGFtcGxl" }
],
"authenticatorSelection": {
"residentKey": "required",
"userVerification": "required"
},
"attestation": "none",
"hints": ["client-device"]
}
| Field | Meaning | Spec |
|---|---|---|
rp.id |
The RP ID: a registrable domain suffix of the page's origin. Every credential is scoped to it for life. | §5.4.2, §6.1 |
user.id |
The user handle: opaque bytes, at most 64, with no personal data. Returned at login as userHandle. |
§5.4.3 |
user.name, displayName |
Shown in the account picker. | §5.4.3 |
challenge |
Server-generated random bytes; at least 16 bytes, single-use, stored server-side. | §5.4, §13.4.3 |
pubKeyCredParams |
Acceptable COSE algorithms in preference order: -7 is ES256, -257 is RS256. | §5.3, §5.8.5 |
excludeCredentials |
Credentials this user already has, so the same authenticator is not registered twice. | §5.4, §5.8.3 |
authenticatorSelection.residentKey |
required creates a discoverable credential, which is what makes it a passkey. |
§5.4.4, §5.4.6 |
userVerification |
required demands PIN or biometric. Still verify the UV flag yourself. |
§5.8.6 |
attestation |
none, indirect, direct or enterprise. Ask for attestation only if you will verify it. |
§5.4.7 |
hints |
Level 3 UI hints: security-key, client-device, hybrid. |
§5.8.8 |
Message 2: the registration response
{
"id": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
"rawId": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
"type": "public-key",
"authenticatorAttachment": "platform",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicTJUYjNwSm0wdzhIY1h5TzduVjVmQTlzTGsxZVJ1NFoiLCJvcmlnaW4iOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjF...",
"transports": ["internal", "hybrid"]
},
"clientExtensionResults": { "credProps": { "rk": true } }
}
Decoded, clientDataJSON is:
{
"type": "webauthn.create",
"challenge": "q2Tb3pJm0w8HcXyO7nV5fA9sLk1eRu4Z",
"origin": "https://login.example.com",
"crossOrigin": false
}
And the attestationObject is a CBOR map { "fmt": "none", "attStmt": {}, "authData": <bytes> } (§6.5.4). The authData bytes break down as:
| Bytes | Field | Example | Spec |
|---|---|---|---|
| 32 | rpIdHash |
SHA-256 of example.com |
§6.1 |
| 1 | flags |
0x5D: UP (bit 0), UV (bit 2), BE (bit 3), BS (bit 4), AT (bit 6) set |
§6.1 |
| 4 | signCount |
0x00000000 (synced passkeys usually report 0) |
§6.1.1 |
| var | attestedCredentialData |
AAGUID (16 bytes), credential ID length and ID, COSE public key | §6.5.1 |
| var | extensions |
CBOR map, present only if ED (bit 7) is set | §6.1 |
The BE and BS bits are how a relying party knows a credential is synced: BE means backup eligible, BS means currently backed up (§6.1.3).
Messages 4 and 5: authentication
{
"challenge": "N7vKq3XcM2pT0aYwR9sBe5LhUj8DfG1o",
"rpId": "example.com",
"allowCredentials": [],
"userVerification": "required",
"timeout": 300000
}
An empty allowCredentials lets the user pick any discoverable credential for this RP ID, which is the usernameless passkey flow; combine it with mediation: "conditional" for autofill (§5.5, §5.1.4).
{
"id": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
"rawId": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
"type": "public-key",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiTjd2S3EzWGNNMnBUMGFZd1I5c0JlNUxoVWo4RGZHMW8iLCJvcmlnaW4iOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
"authenticatorData": "o3mm9u6vuaVeN4wRgDTidR5oL6ufLTCrE9ISVYbOGUcdAAAAAA",
"signature": "MEUCIQDexample-signature-bytes-not-realAiB0example",
"userHandle": "dS0xMjM0NS1leGFtcGxlLW9wYXF1ZQ"
},
"clientExtensionResults": {}
}
| Field | Meaning | Spec |
|---|---|---|
clientDataJSON |
Decodes to type: webauthn.get, the challenge, the origin and crossOrigin; topOrigin appears for cross-origin iframes. |
§5.8.1 |
authenticatorData |
37 bytes here: rpIdHash, flags, signCount; no attested credential data at login. |
§6.1 |
signature |
Over authenticatorData ‖ SHA-256(clientDataJSON), with the credential's private key. |
§6.3.3, §7.2 |
userHandle |
The user.id from registration; required for usernameless flows. |
§5.2.2 |
The checks a relying party must run
Registration (§7.1) and authentication (§7.2) are numbered algorithms. The checks that carry the security:
typeiswebauthn.createat registration andwebauthn.getat login (§7.1, §7.2).challengeequals the base64url of the challenge you generated and stored for this ceremony, and is then discarded (§13.4.3). Never accept a challenge the client supplies.originis one you expect, exactly; checkcrossOriginandtopOriginif you allow iframes (§13.4.9). This check is what makes WebAuthn phishing-resistant.rpIdHashequals SHA-256 of your RP ID (§7.1, §7.2).- Flags. UP is set; UV is set whenever your policy requires user verification; if BE is not set, BS must not be set (§7.2).
- Credential ownership. At login the credential ID belongs to the identified user, or, when the user was not identified first,
userHandlemaps to an account holding that credential ID (§7.2). At registration the credential ID is at most 1023 bytes and not already registered to anyone (§7.1). - Signature verifies with the stored public key, whose
algwas one you offered inpubKeyCredParamsat registration (§7.1, §7.2). - State update. Store the new
signCountand backup state; treat a non-zero counter that fails to increase as a possible clone signal (§6.1.1). Verify attestation only if you requested it and act on the result (§13.4.4).
Common implementation mistakes
| Mistake | What goes wrong | Real examples |
|---|---|---|
| Credential not tied to the account being logged into | Attacker completes a passkey login as the victim | CVE-2025-26788 (StrongKey FIDO Server, discoverable and non-discoverable credentials confused) |
| Assertions or challenges leaked and not strictly single-use | Captured assertion material is replayed | CVE-2026-34348 (assertions in Windows event logs, replayed against Entra ID) |
| Origin or RP ID not checked, or a wildcard origin list | The phishing resistance disappears | AiTM phishing proxy succeeds again |
| UV flag ignored | A touch-only assertion satisfies a policy that assumed PIN or biometric | Check UV server-side, not only in options |
| Passkey added but password or SMS left as fallback | Attackers phish the fallback | OTP relay social engineering, help desk social engineering |
| Endpoint already compromised | Malware extracts key material or drives an authenticator the user already opened | Credential manager key extraction |
| Session cookie not protected after login | The passkey protected sign-in, not the session | Session cookie theft |
The broader table of WebAuthn deployment mistakes, including AAGUID trust and recovery design, is on the standard page.
Try it
Run a registration and a login against a test relying party in the WebAuthn tester, then inspect the decoded clientDataJSON and authenticator flags it shows and compare them with the tables above. To see which business apps let you require passkeys at all, use the MFA matrix. The add passkeys with WebAuthn recipe covers server code.
Related
WebAuthn / FIDO2 standard page, passkeys 101, implementing passkeys in the enterprise, OIDC message flow, Phishing-Resistant MFA Support Matrix 2026. Glossary: WebAuthn, passkey, synced passkey, FIDO2, relying party.
Frequently asked questions
- What data does a WebAuthn registration send to the server?
- The browser returns a PublicKeyCredential with an id and rawId (the credential ID), clientDataJSON (type webauthn.create, the server's challenge and the page origin), an attestationObject (a CBOR map with fmt, attStmt and authData, which holds the RP ID hash, flags, signature counter and the new public key), and transports. The server checks the client data and authenticator data against WebAuthn Level 3 section 7.1 and stores the credential ID and public key against the user.
- What does a passkey sign during login?
- The authenticator signs the binary concatenation of its authenticator data and the SHA-256 hash of clientDataJSON. Because clientDataJSON contains the server's one-time challenge and the origin the browser actually saw, and the authenticator data contains the hash of the RP ID, a signature captured on a phishing domain fails verification on the real site.
- Which checks must a WebAuthn relying party perform on login?
- WebAuthn Level 3 section 7.2 requires the relying party to confirm the credential belongs to the user, that clientDataJSON type is webauthn.get, that the challenge equals the one it issued, that the origin is expected, that rpIdHash equals the SHA-256 of its RP ID, that the user present flag is set and the user verified flag is set when required, that the signature verifies with the stored public key, and then to update the signature counter and backup state.
- Is a passkey the same as WebAuthn?
- A passkey is a discoverable WebAuthn credential, usually synced by a platform or password manager, though device-bound passkeys on security keys exist too. The messages on this page are the same for both; the BE and BS flags in authenticator data tell the relying party whether the credential is backup eligible and currently backed up.
Related on Start with Identity
- GuideHow OAuth 2.0 and OAuth 2.1 Work, Message by Message
OAuth on the wire: the authorization code request with PKCE, the token request and response, client credentials, refresh with rotation, the API call, JWT access
- GuideHow Kerberos Works, Message by Message
Kerberos V5 on the wire: AS-REQ, pre-authentication, AS-REP, TGS-REQ and TGS-REP, AP-REQ and AP-REP, with each ASN.1 field tied to its RFC 4120 section, the che
- GuideHow LDAP Works, Message by Message
LDAP on the wire: StartTLS, the service-account bind, the user search, the user bind and the result codes, with each LDAPMessage field tied to RFC 4511, RFC 451
- ArticlePasswordless CIAM: Passkeys and WebAuthn for Customer Login
How to bring passwordless authentication to customer login: where passkeys fit, how they differ from magic links and OTP, the account-recovery problem, and a ro
- BlogAmazon says 175 million customers now sign in with passkeys
Amazon reports more than 175 million customers have enabled passkeys, signing in six times faster, with passkeys now the default on mobile for anyone who has se
- BlogAmazon says more than 500 million customers have turned on passkeys
Amazon updated its passkey announcement on October 20 to say more than 500 million customers have enabled passkeys, which it says sign in about six times faster