Implementing Ensure in the YubiKey WebAuthn Demo Server

Live demo

This example demonstrates how to integrate Ensure into the YubiKey WebAuthn demo server. Link to YubiKey Walkthrough

We will implement Ensure just after the authentication process is completed on the client side and before the client sends the response to the server for validation, allowing the user to become an authenticated user. We will add the Ensure compliance check to verify that the user's device is compliant.

Step one: Install the demo server. For details, check out the YubiKey Walkthrough.

git clone https://github.com/Yubico/java-webauthn-server
cd java-webauthn-server
./gradlew run

Direct browser to https://localhost:8443/

We are going to modify the file java-webauthn-server/webauthn-server-demo/src/main/webapp/index.html

Install the Ensure library

Add the following script to the <head> section

<script type="text/javascript" src="https://v2.alertsec.com/assets/js/ensure.js"></script>

Run the Ensure library

Add the following function to the end of the main <script> tag.

async function authenticateWithEnsure(event, username) {

    // Make request to the server to get the challange.
    const { request, actions: urls } = await fetch("/api/v1/authenticate", {
        body: new URLSearchParams({ username }),
        method: 'POST',
    }).then(response => response.json()).catch(error => {
        console.error(error)
    });

    // Perform webauthn request with the publicKeyCredentialRequestOptions
    const webauthnResponse = await webauthnJson.get({ publicKey: request.publicKeyCredentialRequestOptions });

    // Run Ensure validation
    const ensureResponse = await Ensure.run("e5437cbcad1f7fe7316179f0dd9fe2c2-62445964538fa-1938241297", username)

    // Submit ensure and webauthn response to server.
    const body = {
        requestId: request.requestId,
        ensureToken: ensureResponse.verification.token,
        credential: webauthnResponse,
        sessionToken: request.sessionToken || session.sessionToken || null,
    };

    const data = await fetch(urls.finish, {
        method: 'POST',
        body: JSON.stringify(body),
    })
        .then(response => response.json())
        .then(updateSession)
    ;

    if (data && data.success) {
        setStatus("Success!");
    } else {
        setStatus('Error!');
    }
    showServerResponse(data);
}

Then modify the function:

function authenticateWithUsername(event) {
    return authenticate(event, document.getElementById('username').value);
}

to use the authenticateWithEnsure function like this:

function authenticateWithUsername(event) {
    return authenticateWithEnsure(event, document.getElementById('username').value);
}

Verify compliance status server side

The authenticateWithEnsure function sends a new parameter ensureToken to the server. This is a temporary ID that can be used for retreiving the compliance status server side for validation. Follow these steps to modify the server to check the compliance status server side.

Step 1

Modify the file webauthn-server-demo/src/main/java/demo/webauthn/data/AssertionResponse.java to accept the ensureToken in the payload.

public class AssertionResponse {

  public final String ensureToken;

  private final ByteArray requestId;
  private final PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
      credential;

  public AssertionResponse(
      @JsonProperty("ensureToken") String ensureToken,
      @JsonProperty("requestId") ByteArray requestId,
      @JsonProperty("credential")
          PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs>
              credential) {
    this.requestId = requestId;
    this.credential = credential;
    this.ensureToken = ensureToken;
  }
}

Step 2

Add these imports to the file webauthn-server-demo/src/main/java/demo/webauthn/WebAuthnServer.java

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

Step 3

In the function finishAuthentication in the same file add the following code right after the first try catch statement.

System.out.println("============ CHECKING ENSURE ================");

try {
    HttpClient ens_client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .build();

    HttpRequest ens_request = HttpRequest.newBuilder()
        .GET()
        .uri(URI.create("https://ensure-agent.alertsec.com/checks/temp/" + ensureId))
        .header("Content-Type", "application/json")
        .build();

    HttpResponse<String> ens_response = ens_client.send(ens_request, HttpResponse.BodyHandlers.ofString());
    String responseBody = ens_response.body();

    boolean firewallCompliant = isFirewallCompliant(responseBody);
    System.out.println("Firewall compliant: " + firewallCompliant);

    if (!firewallCompliant) {
        return Either.left(Arrays.asList("Assertion failed!", "Server side firewall check failed"));
    }

} catch (Exception e) {
    e.printStackTrace();
}

This code makes a request to our api with the temporary token and then verifies if the device has the firewall on.