WebAuthn
ActiveWeb Authentication (WebAuthn) is a W3C Recommendation (Level 2, April 2021) that enables passwordless authentication and multi-factor authentication in web browsers using public-key cryptography. Users authenticate with a hardware security key, platform authenticator (Touch ID, Face ID, Windows Hello), or passkey stored in a password manager. WebAuthn is the browser half of the FIDO2 standard – CTAP2 defines how the browser communicates with the authenticator device.
In one line
WebAuthn (W3C Level 2, 2021) enables passwordless and phishing-resistant authentication via public-key cryptography. Two flows: credential creation (navigator.credentials.create()) registers a key pair on the authenticator; credential assertion (navigator.credentials.get()) proves possession of the private key. All authenticator data is CBOR-encoded. Backed by hardware (YubiKey, Touch ID, Face ID, Windows Hello) or software passkeys (iCloud Keychain, Google Password Manager). Passkeys sync across devices via the platform credential store.
Quick Reference
| Field | Size | Description |
|---|---|---|
| Two flows | Create + Get | Registration: navigator.credentials.create({publicKey: ...}) – creates a new credential on the authenticator. Authentication: navigator.credentials.get({publicKey: ...}) – signs a server challenge to prove credential ownership. |
| Relying Party (RP) | Your server | The server that issues challenges and verifies responses. Identified by rpId (the domain name). The authenticator binds credentials to the rpId – a credential created for example.com cannot be used at evil.com. |
| Challenge | Random nonce | A cryptographically random value (16–32 bytes) generated by the server for each authentication attempt. The authenticator signs the challenge, preventing replay attacks. |
| authenticatorData | CBOR binary | CBOR-encoded binary blob from the authenticator. Contains: rpIdHash (SHA-256 of rpId), flags byte (UP=user present, UV=user verified, AT=attestation included, ED=extension data), signCount (monotonic counter), AAGUID (authenticator model identifier), credentialId, credentialPublicKey (COSE key). |
| clientDataJSON | JSON | Signed by the authenticator alongside authenticatorData. Contains: type ('webauthn.create' or 'webauthn.get'), challenge (Base64URL), origin (the current page URL), crossOrigin (bool). Server MUST verify all fields. |
| Attestation | Optional | Proof of authenticator authenticity. Allows the server to verify the authenticator model (e.g., 'this is a YubiKey 5'). Types: none (passkeys typically use this), packed, tpm, android-key, android-safetynet, fido-u2f, apple. Rarely required by consumer apps. |
| COSE key | RFC 8152 | Authenticator public keys are encoded using COSE (CBOR Object Signing and Encryption). Common algorithm identifiers: -7 (ES256, ECDSA P-256), -257 (RS256, RSASSA-PKCS1-v1_5), -8 (EdDSA, Ed25519). |
| User verification | required/preferred/discouraged | Specifies whether the authenticator must verify the user (PIN, biometric). required: always verify (secure). preferred: verify if possible. discouraged: skip verification (single-factor WebAuthn for convenience). |
Key Characteristics
Phishing-resistant
The authenticator cryptographically binds to the rpId (domain). Credentials created for example.com cannot be used on attacker.com even if the user is tricked into visiting a phishing site. This is the core security advantage over passwords and SMS OTP.
No shared secrets
The server stores only the public key. The private key never leaves the authenticator. A server database breach exposes no usable credentials – there is nothing to phish.
Device binding vs passkeys
Hardware authenticators (YubiKey, platform authenticators) create non-exportable credentials bound to one device. Passkeys (platform credentials synced via iCloud/Google) are exportable by design for convenience. Both are phishing-resistant; hardware keys provide stronger assurance.
Passkeys replace passwords
Synced passkeys via iCloud Keychain, Google Password Manager, or 1Password allow users to authenticate on any device without a physical token. Combined with biometric unlock (Face ID, Touch ID), passkeys provide passwordless login with near-zero friction.
Message Format
// WebAuthn Registration (navigator.credentials.create)
const challenge = crypto.getRandomValues(new Uint8Array(32));
const credential = await navigator.credentials.create({
publicKey: {
challenge,
rp: { name: "Example App", id: "example.com" },
user: {
id: crypto.getRandomValues(new Uint8Array(16)),
name: "[email protected]",
displayName: "Alice"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 } // RS256
],
authenticatorSelection: {
userVerification: "required", // require PIN/biometric
residentKey: "required", // discoverable credential (passkey)
},
timeout: 60000,
attestation: "none" // skip attestation for consumer apps
}
});
// Send to server: credential.id, credential.response.clientDataJSON,
// credential.response.attestationObject// WebAuthn Authentication (navigator.credentials.get)
const authChallenge = crypto.getRandomValues(new Uint8Array(32));
const assertion = await navigator.credentials.get({
publicKey: {
challenge: authChallenge,
rpId: "example.com",
userVerification: "required",
// For passkeys (discoverable credentials): omit allowCredentials
// For hardware keys: include allowCredentials with stored credentialId
}
});
// assertion.response.clientDataJSON (JSON)
// assertion.response.authenticatorData (CBOR binary):
// [0-31] rpIdHash: SHA-256("example.com")
// [32] flags: UP=1, UV=1 (user present, user verified)
// [33-36] signCount: 0x00000042 (monotonic counter)
// assertion.response.signature: ECDSA signature over
// SHA-256(authenticatorData + SHA-256(clientDataJSON))
// Server verification steps:
// 1. Verify clientDataJSON.type === "webauthn.get"
// 2. Verify clientDataJSON.challenge === issued challenge
// 3. Verify clientDataJSON.origin === "https://example.com"
// 4. Verify rpIdHash === SHA-256("example.com")
// 5. Verify flags: UP=1 (user present required), UV=1 if required
// 6. Verify signature with stored public key
// 7. Verify signCount > stored signCount (replay detection)