Using WebAuthn APIs

HYPR SDK for FIDO2 and WebAuthn

This document contains code samples for using the WebAuthn APIs to register and authenticate users.

Checking WebAuthn Browser Support

function isWebAuthnSupported() {
  if (
    window.PublicKeyCredential === undefined ||
    typeof window.PublicKeyCredential !== "function"
  ) {
    return false;
  } else {
    return true;
  }
}

Checking Browser Platform Authenticator Support

PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
  .then(function (available) {
    if (available) {
      alert("PLATFORM AUTH AVAILABLE");
    } else {
      // Use another kind of authenticator or a classical login/password
      // workflow
      alert("PLATFORM NOT AVAILABLE");
    }
  })
  .catch(function (err) {
    // Something went wrong
    console.error(err);
  });

Registration

Registration in FIDO2 is referred to as Attestation.

Registration with FIDO2 is accomplished with two calls:

  • An options call that provides a challenge
  • A result call that completes the registration

The code presented here registers a new user. By default it uses platform authenticators with a required resident key.

function doAttestation(username, displayName, fidoServerURL) {
  const attestationOptions = {
    username: username,
    displayName: displayName,
    authenticatorSelection: {
      authenticatorAttachment: "platform",
      userVerification: "preferred",
      requireResidentKey: true,
    },
    attestation: "none",
  };

  console.log("Starting Attestation....");
  console.log(JSON.stringify(attestationOptions, null, 2));

  fetch(fidoServerURL + "/rp/api/versioned/fido2/attestation/options", {
    method: "POST",
    cache: "no-cache",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(attestationOptions),
  })
    .then((res) => res.json())
    .then((resp) => {
      console.log(
        "/rp/api/versioned/fido2/attestation/options RESPONSE from the server :: "
      );
      console.log(JSON.stringify(resp, null, 2));
      //Use the FIDO2 APIs provided by the browser
      return navigator.credentials.create(makePublicKey(resp));
    })
    //This is executed when the browser has created the credential.
    .then((res) => {
      if (res) {
        console.log("The FIDO2 Credential Has Been Created!");
        console.log(res);
        let attResult = {
          id: res.id,
          rawId: toBase64URL(btoa(bufferToString(res.rawId))),
          type: "public-key",
          response: {
            clientDataJSON: toBase64URL(
              btoa(bufferToString(res.response.clientDataJSON))
            ),
            attestationObject: toBase64URL(
              btoa(bufferToString(res.response.attestationObject))
            ),
          },
        };

        console.log("Doing the FIDO2 Result Call to Complete the Attestation");
        console.log(JSON.stringify(attResult, null, 2));

        return fetch(
          fidoServerURL + "/rp/api/versioned/fido2/attestation/result",
          {
            method: "POST",
            cache: "no-cache",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify(attResult),
          }
        );
      } else {
        console.error(
          "Result from server is undefined during attestation options request."
        );
      }
    })
    .then((res) => res.json())
    .then((resp) => {
      if (resp) {
        console.log("Attestation Completed, Sending Result to FIDO2 RP");
        console.log(resp);
        console.log(JSON.stringify(resp, null, 2));

        if (resp.status === "ok") {
          console.log("Attestation Completed!", resp);
          alert("Attestation Completed!");
        } else {
          console.error(`Error from FIDO2 Server: ${resp.errorMessage}`);
          alert(`Error from server: ${resp.errorMessage}`);
        }
      } else {
        console.error(
          "Result from server is undefined during attestation result request."
        );
      }
    })
    .catch((err) => {
      console.error("Error doing Attestation", err);
    });
}

function makePublicKey(attOptionsResp) {
  if (attOptionsResp.excludeCredentials) {
    attOptionsResp.excludeCredentials = attOptionsResp.excludeCredentials.map(
      function (cred) {
        cred.id = base64ToArrayBuffer(fromBase64URL(cred.id));
        cred.transports = ["internal", "usb", "ble", "nfc"];
        return cred;
      }
    );

    console.log("Attestation Options With Updated Exclude Credentials.");
    console.log(attOptionsResp);
  }

  return {
    publicKey: {
      attestation: attOptionsResp.attestation,
      authenticatorSelection: attOptionsResp.authenticatorSelection,
      excludeCredentials: attOptionsResp.excludeCredentials,
      // Relying Party
      rp: attOptionsResp.rp,
      // User
      user: {
        id: base64ToArrayBuffer(fromBase64URL(attOptionsResp.user.id)),
        name: attOptionsResp.user.name,
        displayName: attOptionsResp.user.displayName,
      },
      // Requested format of new keypair
      pubKeyCredParams: attOptionsResp.pubKeyCredParams,
      timeout: attOptionsResp.timeout,
      challenge: base64ToArrayBuffer(fromBase64URL(attOptionsResp.challenge)),
    },
  };
}

The WebAuthn Credentials Create API

The code above uses the WebAuthn Credentials Create API in the following way:

navigator.credentials.create(makePublicKey(resp));

This API creates the public/private key pair for the user on this particular browser/machine. The resulting key is then sent to the FIDO2 server to be used for authentication purposes.

Authentication

Authentication in FIDO2 is referred to as Assertion.

Authentication with FIDO2 is accomplished using two calls:

  • An options call that provides a challenge
  • A result call that completes the authentication

The code presented below authenticates a previously registered user. By default it uses platform authenticators. It also overrides the transport to be "internal" so that the FIDO client is only going to search and use platform authenticators, even if the user has external authenticators registered.

function doAssertion(username = "") {
  //This is the options request
  let authnOptions = {
    "username": username,
    "userVerification": "preferred",
    "authenticatorSelection": {
      "authenticatorAttachment": "platform"
    }
  };

  console.log("Doing assertion with Authenticator Options: " + JSON.stringify(authnOptions, null, 2));

  fetch(<YOUR_FIDO2_SERVER_URL> + "/rp/api/versioned/fido2/assertion/options", {
    method: "POST",
    cache: "no-cache",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify(authnOptions)
  })
    .then(res => res.json())
  // ******** Server Assertion options response ********
    .then(resp => {
    console.log("Completed the Assertion options request...", JSON.stringify(resp, null, 2));
    // Map credentials id to BufferSource
    resp.allowCredentials = resp.allowCredentials || [];
    let mappedAllowCreds = resp.allowCredentials.map(x => {

      if(isInternalOnlyTransport){
        x.transports = ["internal"]
      }

      return {
        id: base64ToArrayBuffer(fromBase64URL(x.id)),
        type: x.type,
        transports: x.transports
      }
    });

    return navigator.credentials.get({
      publicKey: {
        challenge: base64ToArrayBuffer(fromBase64URL(resp.challenge)),
        timeout: resp.timeout,
        rpId: resp.rpId,
        userVerification: resp.userVerification,
        allowCredentials: mappedAllowCreds
      }
    }).catch(err => {
      console.error("Error processing credential GET request for WebAuthn with code: " + err.code + " and message: " + err.message + " and name: " + err.name);
    });
  })

  // ******** Pass authenticator response to the server ********
    .then(resp => {
    console.log("Authenticator auth response: ", resp);

    if(resp) {
      let authReq = {
        id: resp.id,
        rawId: toBase64URL(btoa(bufferToString(resp.rawId))),
        type: resp.type,
        response: {
          authenticatorData: toBase64URL(btoa(bufferToString(resp.response.authenticatorData))),
          clientDataJSON: toBase64URL(btoa(bufferToString(resp.response.clientDataJSON))),
          signature: toBase64URL(btoa(bufferToString(resp.response.signature))),
          userHandle: toBase64URL(btoa(bufferToString(resp.response.userHandle)))
        }
      };

      console.log("Making Assertion result request to the server with: " + JSON.stringify(authReq, null, 2));

      //Make a FIDO2 call to the assertion result endpoint to process the authenticator result.
      return fetch(<YOUR_FIDO2_SERVER_URL> + "/rp/api/versioned/fido2/assertion/result", {
        method: "POST",
        cache: "no-cache",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(authReq)
      })
    }
  })
    .then(function (res) {
    return res
  })
    .then(res => res.json())
    .then(resp => {

    console.log("FIDO2 Assertion Response Received...", resp);
    if(resp){
      if (resp.status === 'ok') {
        alert("Authentication Completed for: " + resp.username);
      } else {
        //Do error handling here, we simply alert on the error.
        //console.error("Error Processing Assertion: ", resp);
        alert(`Error from server processing Assertion: ${resp}`)
      }
    }
  })
    .catch(err => {
    let msg = err.message ? err.message : err;
    console.error("Error processing assertion: ", err);
  });
}

The FIDO2 Credential Get WebAuthn API Call

The code above uses the WebAuthn Get Credential API to sign the challenge issued by the FIDO2 server in the following way:

navigator.credentials.get({
    publicKey: {
        challenge: base64ToArrayBuffer(fromBase64URL(resp.challenge)),
        timeout: resp.timeout,
        rpId: resp.rpId,
        userVerification: resp.userVerification,
        allowCredentials: mappedAllowCreds
    }
})

This API call will then prompt the user to authenticate on their client. If a known username is provided to the FIDO2 server in the "options" request, then mappedAllowCredentials will be populated with the User IDs of registered authenticators. If a known username is NOT provided in the "options" request then the WebAuthn client will look for resident keys and ask the user to authenticate if it finds them.

Getting Registered Devices for FIDO2

This code gives you the ability to get the registered FIDO2 devices for a particular username:

/**
The userInfoApi URL is: <YOUR_FIDO2_API_URL> + "/rp/api/versioned/fido2/user/info"
**/
function getFido2DeviceForUser(user, userInfoApiUrl, callback) {
  let deviceInfoReq = {
    username: user,
  };

  fetch(userInfoApiUrl, {
    method: "POST",
    cache: "no-cache",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + accessToken,
    },
    body: JSON.stringify(deviceInfoReq),
  })
    .then((response) => response.json())
    .then((data) => {
      console.log("Authenticators Registered for: " + user, data);
      callback(data);
    })
    .catch((error) => {
      console.error("Error Getting Authenticators for: " + user, error);
      callback(error);
    });
}

Deleting a Registered FIDO2 Device for a User

The following JavaScript code allows you to to delete a device registration for a particular username:

/**
The userInfoApi URL is: <YOUR_FIDO2_API_URL> + "/rp/api/versioned/fido2/user/info"

The keyId value is returned from the getFido2DeviceForUser function above.
**/
function deleteDeviceRegistration(keyId, fido2ServerUrl) {
  fetch(fido2ServerUrl + "/rp/api/versioned/fido2/user/" + keyId, {
    method: "DELETE",
    cache: "no-cache",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + accessToken,
    },
  })
    .then((response) => response.json())
    .then((data) => {
      alert(
        "keyId (" +
          keyId +
          ") has been removed from FIDO2 Server: " +
          JSON.stringify(data)
      );
      console.log("Authenticator Deregistered for: " + keyId, data);
    })
    .catch((error) => {
      alert("Error removing keyId (" + keyId + ") from FIDO2 Server: " + error);
      console.error("Error Deregistering Authenticator for: " + keyId, error);
    });
}

WebAuthn Error Information

Duplicate Authenticator Registration Error

If you try to register the same authenticator twice on the same machine for a particular user, you will encounter a InvalidStateError error such as the one below:

❗️

InvalidStateError

Error processing credential CREATE request for WebAuthn with code: 11 and message: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party. and name: InvalidStateError

Authenticator Missing or Timeout Error

This error will occur for one of the following cases:

  • Trying to authenticate using an unregistered authenticator
  • Authentication times out
  • The user cancels the authentication in the WebAuthn user interface

❗️

NotAllowedError

Error processing credential GET request for WebAuthn with code: 0 and message: The operation either timed out or was not allowed. See: https://w3c.github.io/webauthn/#sec-assertion-privacy and name: NotAllowedError.