WebAuthn uses public key cryptography (asymmetric) instead of passwords or SMS texts for registration, authentication and 2FA.

  • Protection against phishing: webauthn signatures changes with the origin, so it won’t work on “similar” webpages (with different domain name).
  • Reduced impact of data breaches: it does not really matter if the public key is stolen.
  • Invulnerable to password attacks: much harder to crack it by “brute force” than passwords.

WebAuthn Browser API

  • navigator.credentials.create() [with publicKey] - creates new credentials (either for registration or for associating a new key pair with an existing account)
  • navigator.credentials.get() [with publicKey] - uses an existing set of credentials to authenticate to a service (either for logging in or as a form of 2FA)

Note: both calls require secure context (https or localhost).

Basically, both calls receive a big random number (challenge) from the server, and return it signed by the private key. This signature can be verified by the server using the public key (which is part of to the create response).

Components

The browser (that implements the above API) sits between the following two components:

  • Server - also referred as RP (relaying party).
  • Authenticator - where the credentials are created and stored. It might be integrated into the browser, into the operating system, or it may be a physical token (like an USB security key).

Registration

WebAuthn registration - source: MDN

( step 0: Some kind of initial registration request.)

  1. Server sends back the challenge, user info and relaying party Info to the JS app. (Form of this communication is not part of the Web Authentication API, it can be basically anything.) The parameters then are passed to the create() call which returns a promise that will resolve into a PublicKeyCredential object containing AuthenticatorAttestationResponse.
  2. Browser validates the parameters, fills in defaults and calls authenticatorMakeCredential() on the authenticator. This will become later the AuthenticatorResponse.clientDataJSON which can be later verified by the server (the origin for example).
  3. The authenticator creates new key pair and attestation. It typically asks for some form of user verification - fingerprint, iris scan, PIN, … - to prove that the user is present and consenting to the registration. Then it creates the key pair, and stores the private part safely. The public key will be part of the response. The data is then signed with a private key that can be validated using a certificate chain.
  4. Authenticator returns data to the browser - the new public key, a globally unique credential id and other attestation is returned => attestationObject.
  5. Final data is created and sent back to the server - the create() call resolves to a PublicKeyCredential object, which is then sent back to the server (using whatever form of communication available).
  6. The server validates the data and finishes the registration process:
    • it verifies that the challenge is the same as the one sent
    • checks the origin
    • validates the signature over the clientDataHash and attestation using the certificate chain for that specific authenticator

Authentication

The authentication process is very similar, the key differences are:

  1. authentication does not require user or relaying party information
  2. it creates an assertion using the previously created key pair (rather then an attestation using the burned in keys of the authenticator)

WebAuthn authentication - source: MDN

( step 0: Some kind of initial registration request.)

  1. Server sends back the challenge.
  2. Browser validates the parameters, fills in defaults and calls authenticatorGetCredential() on the authenticator.
  3. The authenticator creates an assertion using the credentials stored for the given Relaying Party ID and prompts the user to consent to the authentication.
  4. Authenticator returns data to the browser - the authenticatorData and the assertion signature.
  5. Final data is created and sent back to the server - the get() call resolves to a PublicKeyCredential object, which is then sent back to the server (using whatever form of communication available).
  6. The server validates the data and finishes the login process:
    • it verifies the signature using the public key stored during the registration process
    • checks that the Relying Party ID is the one expected
    • checks whether the signed challenge is the same that was generated by the server