# How WebAuthn and Passkeys Work, Message by Message

Source: https://startwithidentity.com/guides/protocols/webauthn-message-flow/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://www.w3.org/TR/webauthn-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](https://startwithidentity.com/standards/webauthn-fido2/). This page is the wire-level detail underneath it.

## The sequence at a glance

| # | Ceremony | Message | Spec |
| --- | --- | --- | --- |
| 1 | Registration | Server → page: `PublicKeyCredentialCreationOptions` | [§5.4](https://www.w3.org/TR/webauthn-3/#dictionary-makecredentialoptions) |
| 2 | Registration | Page → server: `PublicKeyCredential` with `AuthenticatorAttestationResponse` | [§5.2.1](https://www.w3.org/TR/webauthn-3/#iface-authenticatorattestationresponse) |
| 3 | Registration | Server verifies and stores the credential record | [§7.1](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential) |
| 4 | Authentication | Server → page: `PublicKeyCredentialRequestOptions` | [§5.5](https://www.w3.org/TR/webauthn-3/#dictionary-assertion-options) |
| 5 | Authentication | Page → server: `PublicKeyCredential` with `AuthenticatorAssertionResponse` | [§5.2.2](https://www.w3.org/TR/webauthn-3/#iface-authenticatorassertionresponse) |
| 6 | Authentication | Server verifies the assertion and starts a session | [§7.2](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) |

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](https://www.w3.org/TR/webauthn-3/#sctn-parseCreationOptionsFromJSON)) and `toJSON()`, so the base64url JSON shown below is what actually crosses the network.

## Message 1: creation options

```json
{
  "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](https://www.w3.org/TR/webauthn-3/#dictionary-rp-credential-params), [§6.1](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data) |
| `user.id` | The **user handle**: opaque bytes, at most 64, with no personal data. Returned at login as `userHandle`. | [§5.4.3](https://www.w3.org/TR/webauthn-3/#dictionary-user-credential-params) |
| `user.name`, `displayName` | Shown in the account picker. | [§5.4.3](https://www.w3.org/TR/webauthn-3/#dictionary-user-credential-params) |
| `challenge` | Server-generated random bytes; at least 16 bytes, single-use, stored server-side. | [§5.4](https://www.w3.org/TR/webauthn-3/#dictionary-makecredentialoptions), [§13.4.3](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges) |
| `pubKeyCredParams` | Acceptable COSE algorithms in preference order: -7 is ES256, -257 is RS256. | [§5.3](https://www.w3.org/TR/webauthn-3/#dictionary-credential-params), [§5.8.5](https://www.w3.org/TR/webauthn-3/#sctn-alg-identifier) |
| `excludeCredentials` | Credentials this user already has, so the same authenticator is not registered twice. | [§5.4](https://www.w3.org/TR/webauthn-3/#dictionary-makecredentialoptions), [§5.8.3](https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor) |
| `authenticatorSelection.residentKey` | `required` creates a discoverable credential, which is what makes it a passkey. | [§5.4.4](https://www.w3.org/TR/webauthn-3/#dictionary-authenticatorSelection), [§5.4.6](https://www.w3.org/TR/webauthn-3/#enum-residentKeyRequirement) |
| `userVerification` | `required` demands PIN or biometric. Still verify the UV flag yourself. | [§5.8.6](https://www.w3.org/TR/webauthn-3/#enum-userVerificationRequirement) |
| `attestation` | `none`, `indirect`, `direct` or `enterprise`. Ask for attestation only if you will verify it. | [§5.4.7](https://www.w3.org/TR/webauthn-3/#enum-attestation-convey) |
| `hints` | Level 3 UI hints: `security-key`, `client-device`, `hybrid`. | [§5.8.8](https://www.w3.org/TR/webauthn-3/#enum-hints) |

## Message 2: the registration response

```json
{
  "id": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
  "rawId": "ZXhhbXBsZS1jcmVkZW50aWFsLWlkLTAx",
  "type": "public-key",
  "authenticatorAttachment": "platform",
  "response": {
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoicTJUYjNwSm0wdzhIY1h5TzduVjVmQTlzTGsxZVJ1NFoiLCJvcmlnaW4iOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ",
    "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjF...",
    "transports": ["internal", "hybrid"]
  },
  "clientExtensionResults": { "credProps": { "rk": true } }
}
```

Decoded, `clientDataJSON` is:

```json
{
  "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](https://www.w3.org/TR/webauthn-3/#sctn-generating-an-attestation-object)). The `authData` bytes break down as:

| Bytes | Field | Example | Spec |
| --- | --- | --- | --- |
| 32 | `rpIdHash` | SHA-256 of `example.com` | [§6.1](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data) |
| 1 | `flags` | `0x5D`: UP (bit 0), UV (bit 2), BE (bit 3), BS (bit 4), AT (bit 6) set | [§6.1](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data) |
| 4 | `signCount` | `0x00000000` (synced passkeys usually report 0) | [§6.1.1](https://www.w3.org/TR/webauthn-3/#sctn-sign-counter) |
| var | `attestedCredentialData` | AAGUID (16 bytes), credential ID length and ID, COSE public key | [§6.5.1](https://www.w3.org/TR/webauthn-3/#sctn-attested-credential-data) |
| var | `extensions` | CBOR map, present only if ED (bit 7) is set | [§6.1](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data) |

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](https://www.w3.org/TR/webauthn-3/#sctn-credential-backup)).

## Messages 4 and 5: authentication

```json
{
  "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](https://www.w3.org/TR/webauthn-3/#dictionary-assertion-options), [§5.1.4](https://www.w3.org/TR/webauthn-3/#sctn-getAssertion)).

```json
{
  "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](https://www.w3.org/TR/webauthn-3/#dictionary-client-data) |
| `authenticatorData` | 37 bytes here: `rpIdHash`, flags, `signCount`; no attested credential data at login. | [§6.1](https://www.w3.org/TR/webauthn-3/#sctn-authenticator-data) |
| `signature` | Over `authenticatorData ‖ SHA-256(clientDataJSON)`, with the credential's private key. | [§6.3.3](https://www.w3.org/TR/webauthn-3/#sctn-op-get-assertion), [§7.2](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion) |
| `userHandle` | The `user.id` from registration; required for usernameless flows. | [§5.2.2](https://www.w3.org/TR/webauthn-3/#iface-authenticatorassertionresponse) |

## The checks a relying party must run

Registration ([§7.1](https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential)) and authentication ([§7.2](https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion)) 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](https://www.w3.org/TR/webauthn-3/#sctn-cryptographic-challenges)). 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](https://www.w3.org/TR/webauthn-3/#sctn-validating-origin)). 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](https://www.w3.org/TR/webauthn-3/#sctn-sign-counter)). Verify attestation only if you requested it and act on the result ([§13.4.4](https://www.w3.org/TR/webauthn-3/#sctn-attestation-limitations)).

## 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](https://startwithidentity.com/cves/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](https://startwithidentity.com/cves/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](https://startwithidentity.com/techniques/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](https://startwithidentity.com/techniques/otp-relay-social-engineering/), [help desk social engineering](https://startwithidentity.com/techniques/help-desk-social-engineering/) |
| Endpoint already compromised | Malware extracts key material or drives an authenticator the user already opened | [Credential manager key extraction](https://startwithidentity.com/techniques/credential-manager-key-extraction/) |
| Session cookie not protected after login | The passkey protected sign-in, not the session | [Session cookie theft](https://startwithidentity.com/techniques/session-cookie-theft/) |

The broader table of WebAuthn deployment mistakes, including AAGUID trust and recovery design, is on the [standard page](https://startwithidentity.com/standards/webauthn-fido2/#common-implementation-mistakes-and-attacks).

## Try it

Run a registration and a login against a test relying party in the [WebAuthn tester](https://startwithidentity.com/tools/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](https://startwithidentity.com/tools/mfa-matrix/). The [add passkeys with WebAuthn recipe](https://startwithidentity.com/recipes/add-passkeys-webauthn/) covers server code.

## Related

[WebAuthn / FIDO2 standard page](https://startwithidentity.com/standards/webauthn-fido2/), [passkeys 101](https://startwithidentity.com/guides/authentication/passkeys-101/), [implementing passkeys in the enterprise](https://startwithidentity.com/guides/authentication/implementing-passkeys-enterprise/), [OIDC message flow](https://startwithidentity.com/guides/protocols/oidc-message-flow/), [Phishing-Resistant MFA Support Matrix 2026](https://startwithidentity.com/reports/phishing-resistant-mfa-matrix-2026/). Glossary: [WebAuthn](https://startwithidentity.com/glossary/webauthn/), [passkey](https://startwithidentity.com/glossary/passkey/), [synced passkey](https://startwithidentity.com/glossary/synced-passkey/), [FIDO2](https://startwithidentity.com/glossary/fido2/), [relying party](https://startwithidentity.com/glossary/relying-party/).
