const publicKeyCredentialCreationOptions = {challenge: Uint8Array.from(randomStringFromServer, c => c.charCodeAt(0)),rp: {name: "Duo Security",id: "duosecurity.com",},user: {id: Uint8Array.from("UZSL85T9AFC", c => c.charCodeAt(0)),name: "lee@webauthn.guide",displayName: "Lee",},pubKeyCredParams: [{alg: -7, type: "public-key"}],authenticatorSelection: {authenticatorAttachment: "cross-platform",},timeout: 60000,attestation: "direct"};const credential = await navigator.credentials.create({publicKey:publicKeyCredentialCreationOptions});
challenge: The challenge is a buffer of cryptographically random bytes generated on the server, and is needed to prevent "replay attacks". Read the spec.
rp: This stands for “relying party”; it can be considered as describing the organization responsible for registering and authenticating the user. The id must be a subset of the domain currently in the browser. For example, a valid id for this page is webauthn.guide. Read the spec.
user: This is information about the user currently registering. The authenticator uses the id to associate a credential with the user. It is suggested to not use personally identifying information as the id, as it may be stored in an authenticator. Read the spec.
pubKeyCredParams: This is an array of objects describing what public key types are acceptable to a server. The alg is a number described in the COSE registry; for example, -7 indicates that the server accepts Elliptic Curve public keys using a SHA-256 signature algorithm. Read the spec.
authenticatorSelection: This optional object helps relying parties make further restrictions on the type of authenticators allowed for registration. In this example we are indicating we want to register a cross-platform authenticator (like a Yubikey) instead of a platform authenticator like Windows Hello or Touch ID. Read the spec.
console.log(credential);PublicKeyCredential {id: 'ADSUllKQmbqdGtpu4sjseh4cg2TxSvrbcHDTBsv4NSSX9...',rawId: ArrayBuffer(59),response: AuthenticatorAttestationResponse {clientDataJSON: ArrayBuffer(121),attestationObject: ArrayBuffer(306),},type: 'public-key'}
id: The ID for the newly generated credential; it will be used to identify the credential when authenticating the user. The ID is provided here as a base64-encoded string. Read the spec.
rawId: The ID again, but in binary form. Read the spec.
clientDataJSON: This represents data passed from the browser to the authenticator in order to associate the new credential with the server and browser. The authenticator provides it as a UTF-8 byte array. Read the spec.
attestationObject: This object contains the credential public key, an optional attestation certificate, and other metadata used also to validate the registration event. It is binary data encoded in CBOR. Read the spec.
Parsing and Validating the Registration Data
After the PublicKeyCredential has been obtained, it is sent to the server for validation. The WebAuthn specification describes a 19-point procedure to validate the registration data; what this looks like will vary depending on the language your server software is written in.
Duo Labs has provided full example projects implementing WebAuthn written in Python and Go.
const utf8Decoder = new TextDecoder('utf-8');const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON)// parse the string as an objectconst clientDataObj = JSON.parse(decodedClientData);console.log(clientDataObj){challenge: "p5aV2uHXr0AOqUk7HQitvi-Ny1....",origin: "https://webauthn.guide",type: "webauthn.create"}
The clientDataJSON is parsed by converting the UTF-8 byte array provided by the authenticator into a JSON-parsable string. On this server, this (and the other PublicKeyCredential data) will be verified to ensure that the registration event is valid.
challenge: This is the same challenge that was passed into the create() call. The server must validate that this returned challenge matches the one generated for this registration event.
origin: The server must validate that this "origin" string matches up with the origin of the application.
type: The server validates that this string is in fact "webauthn.create". If another string is provided, it indicates that the authenticator performed an incorrect operation
Example: Parsing the attestationObject
const decodedAttestationObject = CBOR.decode(credential.response.attestationObject);console.log(decodedAttestationObject);{authData: Uint8Array(196),fmt: "fido-u2f",attStmt: {sig: Uint8Array(70),x5c: Array(1),},}
authData:The authenticator data is here is a byte array that contains metadata about the registration event, as well as the public key we will use for future authentications. Read the spec.fmt:This represents the attestation format. Authenticators can provide attestation data in a number of ways; this indicates how the server should parse and validate the attestation data. Read the spec.attStmt:This is the attestation statement. This object will look different depending on the attestation format indicated. In this case, we are given a signaturesigand attestation certificatex5c. Servers use this data to cryptographically verify the credential public key came from the authenticator. Additionally, servers can use the certificate to reject authenticators that are believed to be weak. Read the spec.
Example: Parsing the authenticator data
const {authData} = decodedAttestationObject;// get the length of the credential IDconst dataView = new DataView(new ArrayBuffer(2));const idLenBytes = authData.slice(53, 55);idLenBytes.forEach((value, index) => dataView.setUint8(index, value));const credentialIdLength = dataView.getUint16();// get the credential IDconst credentialId = authData.slice(55, 55 + credentialIdLength);// get the public key objectconst publicKeyBytes = authData.slice(55 + credentialIdLength);// the publicKeyBytes are encoded again as CBORconst publicKeyObject = CBOR.decode(publicKeyBytes.buffer);console.log(publicKeyObject){1: 2,3: -7,-1: 1,-2: Uint8Array(32) ...-3: Uint8Array(32) ...}
The authData is a byte array described in the spec. Parsing it will involve slicing bytes from the array and converting them into usable objects.
The publicKeyObject retrieved at the end is an object encoded in a standard called COSE, which is a concise way to describe the credential public key and the metadata needed to use it.
1:The1field describes the key type. The value of2indicates that the key type is in the Elliptic Curve format.3:The3field describes the algorithm used to generate authentication signatures. The-7value indicates this authenticator will be using ES256.-1:The-1field describes this key's "curve type". The value1indicates the that this key uses the "P-256" curve.-2:The-2field describes the x-coordinate of this public key.-3:The-3field describes the y-coordinate of this public key.
navigator.credentials.get()
During authentication the user proves that they own the private key they registered with. They do so by providing an assertion, which is generated by calling navigator.credentials.get() on the client. This will retrieve the credential generated during registration with a signature included.
const credential = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions});
The publicKeyCredentialCreationOptions object contains a number of required and optional fields that a server specifies to create a new credential for a user.
const publicKeyCredentialRequestOptions = { challenge: Uint8Array.from( randomStringFromServer, c => c.charCodeAt(0)), allowCredentials: [{ id: Uint8Array.from( credentialId, c => c.charCodeAt(0)), type: 'public-key', transports: ['usb', 'ble', 'nfc'], }], timeout: 60000,}const assertion = await navigator.credentials.get({ publicKey: publicKeyCredentialRequestOptions});
challenge: Like during registration, this must be cryptographically random bytes generated on the server. Read the spec.
allowCredentials: This array tells the browser which credentials the server would like the user to authenticate with. The credentialId retrieved and saved during registration is passed in here. The server can optionally indicate what transports it prefers, like USB, NFC, and Bluetooth. Read the spec.
timeout: Like during registration, this optionally indicates the time (in milliseconds) that the user has to respond to a prompt for authentication. Read the spec.
Parsing and Validating the Authentication Data
After the assertion has been obtained, it is sent to the server for validation. After the authentication data is fully validated, the signature is verified using the public key stored in the database during registration.
See these projects by Duo Labs for examples of validating the authentication data on the server, written in Python and Go.
Example: Verifying the assertion signature on the server (pseudo-code)
const storedCredential = await getCredentialFromDatabase( userHandle, credentialId);const signedData = ( authenticatorDataBytes + hashedClientDataJSON);const signatureIsValid = storedCredential.publicKey.verify( signature, signedData);if (signatureIsValid) { return "Hooray! User is authenticated! 🎉";} else { return "Verification failed. 😭"}
Verification will look different depending on the language and cryptography library used on the server. However, the general procedure remains the same.
- The server retrieves the public key object associated with the user
- The server uses the public key to verify the signature, which was generated using the
authenticatorDatabytes and a SHA-256 hash of theclientDataJSON