Skip to content
Start with Identity
Protocols · Advanced

How WebAuthn and Passkeys Work, Message by Message

By SWI Community Team · Updated 2026-11-05 · 13 min

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:

  1. type is webauthn.create at registration and webauthn.get at login (§7.1, §7.2).
  2. challenge equals 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.
  3. origin is one you expect, exactly; check crossOrigin and topOrigin if you allow iframes (§13.4.9). This check is what makes WebAuthn phishing-resistant.
  4. rpIdHash equals SHA-256 of your RP ID (§7.1, §7.2).
  5. 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).
  6. Credential ownership. At login the credential ID belongs to the identified user, or, when the user was not identified first, userHandle maps 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).
  7. Signature verifies with the stored public key, whose alg was one you offered in pubKeyCredParams at registration (§7.1, §7.2).
  8. State update. Store the new signCount and 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.

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.
Last reviewed By SWI Community TeamSuggest a correctionHow we research