Skip to main content
WebAuthn

WebAuthn

Active

Web 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.

WebAuthnFIDO2PasskeysW3CPasswordless2FA2019

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

FieldSizeDescription
Two flowsCreate + GetRegistration: 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 serverThe 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.
ChallengeRandom nonceA cryptographically random value (16–32 bytes) generated by the server for each authentication attempt. The authenticator signs the challenge, preventing replay attacks.
authenticatorDataCBOR binaryCBOR-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).
clientDataJSONJSONSigned 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.
AttestationOptionalProof 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 keyRFC 8152Authenticator 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 verificationrequired/preferred/discouragedSpecifies 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

Request
http
// 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
Response
http
// 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)

Implementations

linuxsince simplewebauthn (Node.js), python-fido2 (Python), go-webauthn (Go), webauthn4j (Java). Hardware: YubiKey via libfido2.available
macossince Safari 14+ WebAuthn API. Touch ID via platform authenticator. iCloud Keychain passkeys (macOS 13 Ventura+). YubiKey via USB/NFC.built-in
windowssince Chrome/Edge WebAuthn API. Windows Hello (face/fingerprint/PIN). Microsoft Authenticator passkeys. YubiKey USB.built-in
iossince Safari iOS 14+ WebAuthn. Face ID/Touch ID platform authenticator. iCloud Keychain passkeys (iOS 16+).built-in
androidsince Chrome Android WebAuthn. Biometric authenticator. Google Password Manager passkeys (Android 9+).built-in