WebAuthn Walk-Through

We are going to walk through Yubico’s Java WebAuthn Server library demo registration and authentication ceremonies, explaining how the WebAuthn interaction works, step by step.

Tip
This walk through is designed for people who prefer to learn by doing. If you prefer learning concepts from the ground up, check out our WebAuthn Developer Guide. This walk-through and the guide are complementary to each other.

The walk-through is divided into the following sections:

  • Setup: you will be setting up a local development environment on your computer to provide the starting point for following the walk-through

  • Overview runs through the fundamentals of WebAuthn

  • Registration explains the steps to register an authenticator

  • Authentication explains the steps to sign in to the server.

If you are new to WebAuthn, we recommend you check out https://demo.yubico.com/webauthn-technical/registration.

Prerequisites

We are assuming that you have some familiarity with HTML, JavaScript, and Java. However, you should be able to follow along even if you are coming from a different programming language. We also assume that you are familiar with programming concepts such as functions, objects, arrays, and classes.

Setup

In addition to setting up a local development environment, you also need a YubiKey (or other authenticator).

  1. The following software must be installed to continue with the walk-through:

  2. Verify that you can run the webauthn demo server:

    git clone https://github.com/Yubico/java-webauthn-server
    cd java-webauthn-server
    ./gradlew run
  3. Open https://localhost:8443 to access the WebAuthn demo website.

Note
You will get warnings in your browser about the connection not being secure. This is expected, because this server uses a self-signed certificate. You can safely proceed to the site.

Help, I am stuck!

If you get stuck, you can check Stack Overflow. If you don’t receive an answer, or remain stuck, please file an issue or open a support ticket with Yubico and we will help you out.

Overview

With WebAuthn (also known as FIDO2), public-key cryptography is used to authenticate end-users to an online service, also known as a Relying Party (RP). When the end-user registers for an online service, an RP-specific credential key pair - i.e., a private key and a public key - is generated on the authenticator and the public key is sent to the RP (the private key never leaves the authenticator). When the user makes the request to log in, the authenticator sends an assertion that proves the user possesses the private key. The RP uses the public key to validate the assertion before allowing the user to log in.

Inspecting the code

The WebAuthn demo web app is composed of four layers:

  1. The front end layer, a.k.a. the client, is made up of the index.html along with some JavaScript libraries such as:

    • js/webauthn.js which calls the WebAuthn API methods, and

    • js/base64url.js which handles the ByteArray to Base64 conversions.

  2. The front end interacts with the server via a REST API layer that is implemented in the WebAuthnRestResource class.

  3. The REST API then delegates to the server layer that is implemented in the WebAuthnServer class. This layer is where the business logic lives. The demo server implements "persistent" storage of users and credential registrations using the InMemoryRegistrationStorage class, which implements the CredentialRepository interface.

  4. The server layer calls the webauthn-server-core library layer, which implements the WebAuthn API Relying Party Operations.

Registration

View the demo web app by going to https://localhost:8443. It is a single-page app that demonstrates the WebAuthn registration and authentication ceremonies. In production systems, the WebAuthn authenticator registration functionality is typically located under the security section of the user’s profile page. For simplicity, this demo app combines both registration and authentication on a single page.

Step 1 (Server) Create a credential repository:

The first task of the server is to register and store the user’s WebAuthn credentials in a credential repository. The CredentialRepository interface describes how the server looks up credentials by username or user handle. This demo stores usernames and credentials in memory. For the reference implementation, see the InMemoryRegistrationStorage class.

Step 2 (Server) Set up the WebAuthn relying party:

As the web app starts up, it instantiates a RelyingParty object from the webauthn-server-core library. Your credential repository is passed in as the argument to the .credentialRepository() method. For reference, see the WebAuthnServer class constructor.

public class WebAuthnServer {

    private final RegistrationStorage userStorage;  // The InMemoryRegistrationStorage implements the RegistrationStorage and CredentialRepository interfaces

    private final RelyingParty rp;

    public WebAuthnServer() {
        this(new InMemoryRegistrationStorage(), Config.getRpIdentity(), ...);
    }

    public WebAuthnServer(RegistrationStorage userStorage, RelyingPartyIdentity rpIdentity, ...) {
        this.userStorage = userStorage;
        ...

        rp = RelyingParty.builder()
            .identity(rpIdentity)
            .credentialRepository(this.userStorage)
            ...
            .build();
    }

    ...
}

Step 3 (Client) Send registration request to server:

Now we can initiate a request to register an authenticator via the web app at https://localhost:8443.

  1. Enter a username

  2. Click the Register new account button.

The JavaScript makes a call to the /register endpoint of the REST API to initiate a registration request and passes in the username.

Step 4 (Server) Prepare the registration ceremony parameters:

The server calls the rp.startRegistration() operation, which creates a PublicKeyCredentialCreationOptions JSON object and sets the values based on the service’s policy and preferences. In the following example you can see that the JavaScript app passed in the username "test". The server set the RP ID (rpID) to "localhost". The rpID is important because the client - the browser in this case - protects against spoofing attacks by validating it against the origin’s effective domain. To also protect against replay attacks, the server generates a pseudo-random challenge.

{
    "rp": {
        "name": "Yubico WebAuthn demo",
        "id": "localhost"
    },
    "user": {
        "name": "test",
        "displayName": "test",
        "id": "eShrgFw-m1yWL_VJYKuBqOk2Wcxnkfi1v4adq7Xqr_s"
    },
    "challenge": "g9xJT91T0xXBdsyqDXX9-tfZJBJ1rO6E8Mfiv30VCdg",
    "pubKeyCredParams": [
        {
            "alg": -7,
            "type": "public-key"
        },
        {
            "alg": -8,
            "type": "public-key"
        },
        {
            "alg": -257,
            "type": "public-key"
        }],
    "excludeCredentials": [],
    "authenticatorSelection": {
        "requireResidentKey": false,
        "userVerification": "preferred"
    },
    "attestation": "direct",
    "extensions": {}
}

This registration response is returned to the client. To learn more about this data structure, see PublicKeyCredential Interface and CredentialRequestOptions.

Step 5 (Client) Send registration request to the authenticator:

The JavaScript app calls the method navigator.credentials.create() and passes the PublicKeyCredentialCreationOptions from the /register response. To learn more, see Create a new credential.

At this point the client will prompt the end-user to interact with an authenticator. This experience varies based on browser or operating system. The user will be asked to use a USB security key or a platform built-in sensor. Then the user may be prompted to touch the security key, enter a PIN, and touch the security key again.

The authenticator then generates an RP-specific key-pair. It includes the public key in the AuthenticatorAttestationResponse that is returned from the navigator.credentials.create() method.

Step 6 (Client) Send the authenticator registration response to the server:

The AuthenticatorAttestationResponse has an attestation object with an attestation statement that contains a signature by the private key over the attested credential public key and challenge.

The JavaScript now calls the /register/finish endpoint of the REST API and passes along the AuthenticatorAttestationResponse.

Step 7 (Server) Finish the registration:

Once the server receives the request to finish the registration, it calls the rp.finishRegistration() method with the AuthenticatorAttestationResponse data. The webauthn-server-core parses the authenticator response and verifies that the rpID and challenge are the values it expected. It also verifies the public key and signature. If these are all correct, the server stores the credential ID, credential public key, and signature counter to the database. We recommend storing the raw attestationObject as well for future reference.

To learn more, check out the WebAuthn Client Registration chapter of the WebAuthn Developer Guide.

Authentication

Now that we have registered our credential, let us authenticate with it!

Step 1 (Client) Send the authentication request to the server:

Go to https://localhost:8443 and click the Authenticate button. The JavaScript app makes a call to the /authenticate endpoint of the REST API and passes along the username.

Step 2 (Server) Prepare the authentication ceremony parameters:

The server calls the rp.startAuthentication() operation, which creates a PublicKeyCredentialRequestOptions JSON object and sets the values based on the service’s policy and preferences. Just as in the registration step, the server sets the rpID and challenge. The allowCredentials list is populated with those previously registered credentials that the user is allowed to authenticate with.

{
    "challenge": "kVDORSw87Z4PwuiCKOmQ7lduC-SReKF_TLayhPLBW5c",
    "rpId": "localhost",
    "allowCredentials": [
      {
        "type": "public-key",
        "id": "a_TJPMGXaqyff0ZuEVD3k3bnfiiK049rPnmWSfnNkIFW1vWYaKSgIJpIiuyUChF0Br7MDUxpbKRKVWtGKQv1tA"
      }
    ],
    "userVerification": "preferred",
    "extensions": {
      "appid": "https://localhost:8443"
    }
}

This authentication response is returned to the JavaScript app.

Step 3 (Client) Send the authentication request to the authenticator:

The JavaScript app calls navigator.credentials.get() and passes the PublicKeyCredentialRequestOptions into the method.

At this point the client prompts the user to interact with an authenticator. This experience varies based on browser or operating system. A user is asked to use a USB security key or a platform built-in sensor. The user may be prompted to touch the security key, enter a PIN, and touch the security key again.

The authenticator matches a credential from the allowCredentials list (recall that credentials are scoped to an rpID), uses the associated private key to sign over the authenticator data, and returns an AuthenticatorAssertionResponse to the JavaScript app.

Step 4 (Client) Send the authentication response to the server:

The AuthenticatorAssertionResponse contains authenticator data (rpID and challenge) and the signature by the private key over the authenticator data.

The JavaScript app now calls the /authenticate/finish endpoint of the REST API and passes along the AuthenticatorAssertionResponse.

Step 5 (Server) Finish the authentication:

Once the server receives the request to finish the authentication, it calls the rp.finishAuthentication() method with the AuthenticatorAssertionResponse data. The webauthn-server-core parses the authenticator response and verifies that the rpID and challenge are the values it expected. It also verifies the public key and signature. If these are all correct, the server authenticates the user.

To learn more, check out the “Authentication Flow” section of the Client Authentication chapter of the WebAuthn Developer Guide.

Wrapping Up

Congratulations! You have completed all the steps to register and authenticate with a WebAuthn credential.

If you have more time, we recommend you check out Yubico’s best practices in the integration review standard and review the WebAuthn/FIDO2 Readiness Checklist.