Vert.x FIDO2 webauthn for web applications

This how to will show you how to build and secure a simple FIDO2 CONFORMANT (commonly known as Web Authentication API) vert.x web application. The Web Authentication API (also known as WebAuthn) is a specification written by the W3C and FIDO Alliance, with the participation of Google, Mozilla, Microsoft, Yubico, and others. The API allows servers to register and authenticate users using public key cryptography instead of a password.

What you will build

By the end of this how-to, you will have a working application that requires or stores passwords to perform authentication. This will be achieved with WebAuthn, which is a new W3C global standard for secure authentication on the Web supported by all leading browsers and platforms.

What you need

  • A text editor or IDE

  • Java 8 or higher (11 or >=15 recommended for the extra security algorithms)

  • Internet access

Create a project

Go to start.vertx.io and create a project with the following dependencies:

  • Vert.x Web

  • Webauthn Auth

project

Security first

Web Authentication API is a secure API and vendors decided to follow the best practices. While you can build a server without using SSL, modern web browsers will not connect or allow the usage of the webauthn APIs with servers not using SSL, even during development.

Before we can start using webauthn we will need to ensure that even our development application is SSL ready. For this we need to create a valid SSL certificate. Do note that self signed certificates are still allowed:

To create a self-signed certificate for your IP address do the following:

export IP=10.0.0.2
export CERTSTORE_SECRET=password    # (1)
keytool \
  -genkeypair \
  -alias rsakey \
  -keyalg rsa \
  -storepass ${CERTSTORE_SECRET} \
  -keystore certstore.jks \
  -storetype JKS \
  -dname "CN=${IP}.nip.io,O=Vert.x Development" # (2)
  1. Don’t use this password!

  2. replace the CN with your own IP address (other than localhost) with suffix .nip.io

For this setup we’re relying on a free DNS server that returns your IP address when queried. Other services exist in the web that provide the same results, for example:

At this moment we have a SSL certificate, however the format is considered deprecated for modern Java versions, so we need a second step to convert it to PKCS#12:

keytool \
  -importkeystore \
  -srckeystore certstore.jks \
  -destkeystore certstore.jks \
  -deststoretype pkcs12

Your new SSL certificate is on the file certstore.jks.

Registration Flow

Before being able to login to your application we will need to register an FIDO2 authenticator. This process is similar to a "sign-up" flow in web application. There are however a few differences that this diagram tries to illustrate:

register flow
  1. User registers using just a username

  2. Your server (Relying Party) creates a secure challenge

  3. The browser will pass this information to the token device

  4. The token generates a new key pair for this information

  5. The challenge is signed and returned to the server (RP)

  6. The server verifies that the challenge is correct and stores the public key

Authentication Flow

authn flow
  1. User authenticates using just a username

  2. Your server (Relying Party) creates a secure challenge

  3. The browser will pass this information to the token device

  4. The token generates verifies the credential and signs the challenge

  5. The browser creates an authentication assertion

  6. The server verifies that the assertion is correct and allows the user

Writing the server code

Handling Authenticator Objects

You can find the full source code here, for now let’s just cover important parts.

In order to have some working registration and authentication we need to be able to store and query authenticator data. For this we need to provide a few functions that do exactly this job. You can check it’s source code here. In your verticle you start by creating this object as:

    // Dummy database, real world workloads
    // use a persistent store or course!
    InMemoryStore database = new InMemoryStore();

Configure the Webauthn Object

In order to use FIDO2 we need to configure how the authentication provider shall work. For this we need to create and configure an WebAuthn object:

    // create the webauthn security object
    WebAuthn webAuthN = WebAuthn.create(
      vertx,
      new WebAuthnOptions()   // (1)
        .setRelyingParty(new RelyingParty()
          .setName("Vert.x FIDO2/webauthn Howto"))
        .setUserVerification(UserVerification.DISCOURAGED)  // (2)
        .setAttestation(Attestation.NONE)   // (3)
        .setRequireResidentKey(false)   // (4)
        .setChallengeLength(64)   // (5)
        .addPubKeyCredParam(PublicKeyCredential.ES256)    // (6)
        .addPubKeyCredParam(PublicKeyCredential.RS256)
        .addTransport(AuthenticatorTransport.USB)   // (7)
        .addTransport(AuthenticatorTransport.NFC)
        .addTransport(AuthenticatorTransport.BLE)
        .addTransport(AuthenticatorTransport.INTERNAL))
      // where to load/update authenticators data
      .authenticatorFetcher(database::fetcher)
      .authenticatorUpdater(database::updater);
  1. All configuration happens in the WebAuthnOptions. This is just a small example of sensible defaults, consult the javadocs for even more options.

  2. When verifying users we do not really require then to be verified.

  3. During registration we don’t want to attest the hardware.

  4. We don’t need resident keys for users to authenticate.

  5. Define the length of a challenge, the minimum is 32.

  6. Which security algorithms do we accept.

  7. What kind of transports do we allow from the browser to the authenticator.

Web router initialization

Let’s start configuring our HTTP routes to be secure. Before you can use FIDO2 there are a few handlers that ALWAYS need to be in place:

  • BodyHandler

  • SessionHandler

It is also recommended to have StaticHandler because the whole process requires both vert.x code (the one we’re currently exploring) and a small helper JavaScript script. To simplify development such helper is also provided by vert.x just by using a maven dependency:

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
    </dependency>
    <dependency>    <!--(1)-->
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
      <version>${vertx.version}</version>
      <classifier>client</classifier>
      <type>js</type>
    </dependency>
  </dependencies>
  1. Provided simple helper script to interact with the vert.x backend.

Now we can initialize the web router as:

    final Router app = Router.router(vertx);
    app.route()   // (1)
      .handler(StaticHandler.create());
    app.post()    // (2)
      .handler(BodyHandler.create());
    app.route()   // (3)
      .handler(SessionHandler
        .create(LocalSessionStore.create(vertx)));

    WebAuthnHandler webAuthnHandler = WebAuthnHandler.create(webAuthN) // (4)
      .setOrigin(String.format("https://%s.nip.io:8443", System.getenv("IP")))
      // required callback
      .setupCallback(app.post("/webauthn/callback"))
      // optional register callback
      .setupCredentialsCreateCallback(app.post("/webauthn/register"))
      // optional login callback
      .setupCredentialsGetCallback(app.post("/webauthn/login"));

    app.route()
      .handler(webAuthnHandler);

    app.route("/protected")   // (5)
      .handler(ctx ->
        ctx.response()
          .end(
            "FIDO2 is Awesome!\n" +
              "No Password phishing here!\n"));
  1. Serve the client side application (more on this later).

  2. Enable parsing of POST request body.

  3. Enable sessions.

  4. Mount the webauthn handler using the config defined previously.

  5. Example secure route.

Server boostrap

Now that we have a minimal router we need to create a HTTPS server. Note that this is a required step and the reason why we created a self-signed certificate and use a custom domain name for our development environment.

    vertx.createHttpServer(
      new HttpServerOptions()
        .setSsl(true)
        .setKeyStoreOptions(
          new JksOptions()
            .setPath("certstore.jks")
            .setPassword(System.getenv("CERTSTORE_SECRET"))))
      .requestHandler(app)
      .listen(8443, "0.0.0.0")
      .onSuccess(v -> {
        System.out.printf("Server: https://%s.nip.io:8443%n", System.getenv("IP"));
        start.complete();
      })
      .onFailure(start::fail);

At this moment our backend application is complete and the route /protected shall be secure by FIDO2. Running the application as:

IP=10.0.0.2 \
mvn exec:java

# The following line should be present in your console:
# Server listening at: https://10.0.0.2.nip.io:8443

It is normal that your browser will given you an warning about the self-signed certificate:

selfsigned

This is for your protection. In a real world application you should use a proper SSL certificate such as the ones issued by let’s encrypt.

Navigating with your browser to: https://10.0.0.2.nip.io:8443/protected should render a Forbidden error. The next step is to create a minimal login and registration web application.

Writing the client code

For the client application, no framework is going to be used in order to show that the script can be used with any framework or without.

We will create a minimal HTML page with 3 sections:

  1. a form where users can register or login by just entering their:

    • Display Name: For example a user friendly name such as "John Doe"

    • Username: An unique username such as "[email protected]"

  2. Buttons to register and login

  3. A link to the protected resource, only after register AND login should be allowed.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>WebAuthn Howto</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

<div id="message"></div>
<div> <!--(1)-->
  <label for="displayName">Display Name: </label>
  <input id="displayName" name="displayName" type="text" value="Your Name"><br/>
  <label for="username">Username: </label>
  <input id="username" name="username" type="email" value="[email protected]">
</div>
<hr>
<div> <!--(2)-->
  <button id="register">register</button>
  <button id="login">login</button>
</div>
<hr>
<div> <!--(3)-->
  <a href="/protected">Want some secret info?</a>
</div>

<script src="vertx-auth-webauthn-client.js"></script>   <!--(4)-->
<script src="main.js"></script>   <!--(5)-->
</body>
</html>
  1. The first section as described above.

  2. The second section as described above.

  3. The third section as described above.

  4. Helper script added to the project dependencies

  5. Your application script

The client script code

You can check the full script here. Let’s just cover the important sections.

The included script vertx-auth-webauthn-client.js defines a global type WebAuthn. The first step is to create an instance of this object and configure it to match our backend configuration:

const webAuthn = new WebAuthn({
  callbackPath: '/webauthn/callback',
  registerPath: '/webauthn/register',
  loginPath: '/webauthn/login'
});

The configuration should be straightforward, only define the paths that match your backend routes.

Next we need to add event handlers to our buttons. We start with the register action:

registerButton.onclick = () => {
  webAuthn
    .register({
      name: document.getElementById('username').value,
      displayName: document.getElementById('displayName').value
    })
    .then(() => {
      displayMessage('registration successful');
    })
    .catch(err => {
      displayMessage('registration failed');
      console.error(err);
    });
};

The onclick event will just use the Webauthn object. The webauthn object has only 2 method and it’s Promise based to it should not be hard to grasp for vert.x users. If you’re mainly a Java vert.x developer, just think of a JavaScript Promise as a vert.x Future and everything should be very similar.

To register a user there are just 2 required properties:

  1. name the unique username, for example, an email address.

  2. displayName a human friendly text description, for example, your first and last name.

Filling the form and clicking the register button, the user should be presented with a popup asking to authorize the registration:

register start

After you touch your authenticator the flow should complete successfully with:

register end

At this moment you can close your browser or even open a different one. The sign-up process is complete so you can now login from anywhere with your token.

We now need to handle the onclick event for the login button:

loginButton.onclick = () => {
  webAuthn
    .login({
      name: document.getElementById('username').value
    })
    .then(() => {
      displayMessage('You are logged in');
    })
    .catch(err => {
      displayMessage('Invalid credential');
      console.error(err);
    });
};

Like the FIDO2 describes, using webauthn is a passwordless authentication, so the only required field to perform the login is:

  1. name the unique username, for example, an email address.

login start

Just like in the registration screen a popup will be presented telling you that there is an intent to login and asks the user for authorization. When you touch your authenticator you can see:

login end

Now that you’re logged in, you can finally try to see the secret info link, which shall give your something like:

secret

Summary

In this how-to we covered:

  1. Creating a web project

  2. Secure a web application with Webauthn

  3. Write the client side code with the help of webauthn-client.js

I hope you now can use FIDO2/webauthn on your next project!


Last published: 2021-06-22 00:49:38 +0000.