Building a WebAuthn Application with Java

The Web Authentication (WebAuthn) specification, given official approval by the World Wide Web Consortium (W3C) and the FIDO Alliance in 2019, aims to strengthen online security by allowing users to sign in to sites with elements like biometrics and FIDO security keys. The WebAuthn API can replace or supplement less-secure passwords, which may be weak and are often shared.

WebAuthn is supported by default in Firefox and Chrome browsers and can be used in Edge browsers on recent Windows systems. As support for the new standard is built into more devices, more websites will offer this easier, more secure method of authentication.

In this post, I’ll explain what WebAuthn is, how it works, and we’ll build a Java web application that allows a user to register and log in with a supported WebAuthn device.

Table of Contents

How WebAuthn works

WebAuthn authenticator generates a public-private key pair, using public-key cryptography, scoped to a specific URI to be used for authentication. The service provider must identify the application based on this key. The private key used to generate the credentials is also kept hidden by the authenticator, so the authenticating software and service provider are less susceptible to malicious actors.

There are two distinct steps for using WebAuthn: credential registration and credential authentication. Registration is the process of generating, scoping, and storing a public key for authentication. Both the authenticator and server will store some information about the other party. Authentication is the process of requesting a credential from an authenticator, then verifying its validity.

WebAuthn registration flow

In this application, the user hits a Register button and the server responds with information about the origin of the registration (hostname and origin), a byte-based user handle for identification, a randomly generated challenge nonce, and other optional information about the type of registration the server will accept.

The browser receives this information. A JavaScript function on the client re-encodes and formats the data for the WebAuthn API, and the generated JavaScript object is passed to the browser’s navigator.credentials.create() function.

The WebAuthn API passes the request to a connected authenticator, which normally requests some form of user verification (such as a PIN number or fingerprint), then generates a key pair for future verification and a credential ID to scope the relying party. The public key, credential ID, and challenge nonce, along with optional additional information, are passed back to the service provider; the provider checks the challenge nonce for tampering and then stores the public key and credential ID for future use.

WebAuthn authentication flow

Authentication functions similarly, though the content of the data passed around is slightly different. The user hits a Sign-in button and the service provider sends identifying information (credential ID and a challenge nonce). JavaScript on the client passes that request to the authenticator using the WebAuthn API, then the authenticator verifies the credential and returns a key, which is passed back to the service provider. The service provider verifies the key and performs any steps necessary to grant the user access to appropriate resources, like setting authentication cookies.

How to authenticate using WebAuthn in Java

In this article, you’re going to build a Java application that authenticates users using WebAuthn. To bootstrap a Java web framework that can create and store user sessions, you’ll use Spring Boot to implement the web application, then implement the WebAuthn server, built by the hardware authentication manufacturer Yubico, to create the necessary JSON for the client to create and use credentials.

The final version of the code can be found here. The demo application uses Java 17 and Apache Maven for dependency management.

Get started with WebAuthn and Spring Boot

First, go to the Spring Initializr page and add your dependencies:

  • Lombok: A library used to speed up development by automatically generating constructors, getters and setters.
  • Spring Data JPA: An ORM (Object-relational mapping) tool to persist data and map Java classes to data objects.
  • H2 Database: An in-memory database for simple data persistence. No saved data will exist after application shutdown. For a production-ready application, replace this with a traditional database and edit the spring.datasource.driver-class-name property.
  • Spring Web: To handle web requests.
  • Thymeleaf: For HTML templating.

After creating and downloading this project, add the Yubico WebAuthn server dependency. The application also needs some server-side caching, used to verify a challenge token that’s generated by the server and then provided by the client after a successful authenticator ceremony. This application uses HttpSession to track challenge tokens.

<dependency>
    <groupId>com.yubico</groupId>
    <artifactId>webauthn-server-core</artifactId>
    <version>1.12.1</version>
</dependency>

Spring JPA data layer overview

The application stores two data object classes: users and credentials. Users are people using the system, and they can have multiple credentials. Credentials contain the information needed to identify and verify a device implementing Client to Authenticator (CTAP2) protocol. CTAP2 is a spec that describes communication between a roaming authenticator and another client/platform at the application layer, as well as bindings to a variety of transport protocols that use different physical media.

User data

There are two goals for the user data: track the existence and uniqueness of the user and their credentials, and enable the creation of two JavaScript objects for the browser to make WebAuthn API requests: PublicKeyCredentialCreationOptions for registration and PublicKeyCredentialRequestOptions for authentication.

Using Spring JPA, the AppUser data class starts with traditional fields for organizing users in a system, an id for database lookup, and a unique username to enable user self-identification.

The displayName and handle fields are used by the server to create WebAuthn requests:

import com.yubico.webauthn.data.ByteArray;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;

@Entity
@Getter
@NoArgsConstructor
public class AppUser {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String displayName;

    @Lob
    @Column(nullable = false, length = 64)
    private ByteArray handle;
}

The username field is set by the user. It is intended to be displayed by UI elements on the client as part of the registration and authentication process, but it’s also entirely optional in the WebAuthn process. According to the WebAuthn Specification, a nickname string like username is not suitable for identification, so WebAuthn identity requests are made using a user handle, a byte sequence with a maximum length of 64.

Byte data (like the user handle) is stored in the database as a binary large object (BLOB). To convert these data fields to the ByteArray object the application uses, the application implements a JPA AttributeConverter class with the Converter annotation.

import com.yubico.webauthn.data.ByteArray;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true)
public class ByteArrayAttributeConverter implements AttributeConverter<ByteArray, byte[]> {

    @Override
    public byte[] convertToDatabaseColumn(ByteArray attribute) {
        return attribute.getBytes();
    }

    @Override
    public ByteArray convertToEntityAttribute(byte[] dbData) {
        return new ByteArray(dbData);
    }
}

The application converts data classes into Java objects provided by the Yubico library that can produce JSON-formatted strings the client will pass to the WebAuthn API. The toUserIdentity() function converts a AuthUser data class to a UserIdentity object that can access the username, the screen name, and the all-important user handle. Traditional property getters are created with the Lombok @Getter annotation.

public AppUser(UserIdentity user) {
    this.handle = user.getId();
    this.username = user.getName();
    this.displayName = user.getDisplayName();
}

public UserIdentity toUserIdentity() {
    return UserIdentity.builder()
        .name(getUsername())
        .displayName(getDisplayName())
        .id(getHandle())
        .build();
}

Create a Spring Data CrudRepository to manage the AppUser objects:

import com.yubico.webauthn.data.ByteArray;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<AppUser, Long> {
    AppUser findByUsername(String name);
    AppUser findByHandle(ByteArray handle);
}

Credential data

The server also stores information about the user’s credentials. There is a standard id and a name field used to identify the authenticator on the server side.

To authenticate a credential, the server constructs an AssertionRequest Java object that contains the server-side information about any credential. The AssertionRequest is converted into JSON and sent to the browser, which it turns into PublicKeyCredentialRequestOptions during the WebAuthn authentication ceremony. The required fields are a credentialId, a random byte array created by the authenticator that identifies the scope of a credential. A publicKey is also required, which is another array of bytes. This is generated by the authenticator’s cryptographic algorithm and is used to check the authenticity of a credential signature provided during authentication.

import com.yubico.webauthn.RegistrationResult;
import com.yubico.webauthn.data.AttestedCredentialData;
import com.yubico.webauthn.data.AuthenticatorAttestationResponse;
import com.yubico.webauthn.data.ByteArray;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import java.util.Optional;

@Entity
@Getter
@NoArgsConstructor
public class Authenticator {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String name;

    @Lob
    @Column(nullable = false)
    private ByteArray credentialId;

    @Lob
    @Column(nullable = false)
    private ByteArray publicKey;

    @ManyToOne
    private AppUser user;

    /* The authenticator potentially provides a range of additional information. This 
     * application stores some of it to enable functionality that could be useful for
     * a production-quality web authentication project.
    */

    @Column(nullable = false)
    private Long count;

    @Lob
    @Column(nullable = true)
    private ByteArray aaguid;
}

The W3 recommendation strongly encourages authenticators to implement a signature count field that increments each time the authenticator is used. By storing the 32-bit signCount integer provided by the authenticator, the server can verify how many times the authenticator has been used. Increasing counts are expected; if the authenticator reports a decreasing count, it should raise a red flag.

The aaguid field is an identifier that should be provided by authenticators (but isn’t always), which identifies the type of credential used. This can be used to verify the authenticator’s make and model. Also, it can be useful for denying access for outdated authenticators with known security vulnerabilities.

The credential data class has one constructor that uses the Yubico RegistrationResult and the AuthenticatorAttestationResponse object (converted from JavaScript to Java), along with a username and name for the credential.

public Authenticator(RegistrationResult result, 
                     AuthenticatorAttestationResponse response, 
                     AppUser user, 
                     String name) {
    Optional<AttestedCredentialData> attestationData = response.getAttestation()
        .getAuthenticatorData()
        .getAttestedCredentialData();
    this.credentialId = result.getKeyId().getId();
    this.publicKey = result.getPublicKeyCose();
    this.aaguid = attestationData.get().getAaguid();
    this.count = result.getSignatureCount();
    this.name = name;
    this.user = user;
}

Create another CrudRepository to mange the Authenticator objects.

import java.util.List;
import java.util.Optional;

import com.yubico.webauthn.data.ByteArray;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AuthenticatorRepository extends CrudRepository<Authenticator, Long> {
    Optional<Authenticator> findByCredentialId(ByteArray credentialId);
    List<Authenticator> findAllByUser (AppUser user);
    List<Authenticator> findAllByCredentialId(ByteArray credentialId);
}

Application properties

Create a configuration bean to hold the application’s properties:

import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Getter;
import lombok.Setter;

@Configuration
@Getter
@Setter
@ConfigurationProperties(prefix = "authn")
public class WebAuthProperties {
    private String hostName;
    private String display;
    private Set<String> origin;
}

Add configuration for localhost in your src/main/resources/application.properties:

authn.hostname=localhost
authn.display=Spring Boot WebAuthn Sample Application
authn.origin=http://localhost:8080

Implementing the WebAuthn server library

Now that the data classes exist, the next step is building classes that retrieve and store data from the WebAuthn API. Yubico provides the CredentialRepository interface to handle credential storage and lookup. The demo application’s RegistrationService class overrides the five functions needed for the interface to function.

import com.yubico.webauthn.CredentialRepository;
import com.yubico.webauthn.RegisteredCredential;
import com.yubico.webauthn.data.ByteArray;
import com.yubico.webauthn.data.PublicKeyCredentialDescriptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import lombok.Getter;

import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Getter
@Repository
public class RegistrationService implements CredentialRepository {

    @Autowired
    private UserRepository userRepo;

    @Autowired
    private AuthenticatorRepository authRepository;

    // methods added in next section    
}

Data interface

The getCredentialIdsForUsername() function is used during credential registration. Any associated credential IDs stored in the database are returned as a set of PublicKeyCredentialDescriptor objects. This list of existing credential IDs is passed to the WebAuthn API as a list of excluded credentials to prevent the same credential from being registered twice.

@Override
public Set<PublicKeyCredentialDescriptor> getCredentialIdsForUsername(String username) {
    AppUser user = userRepo.findByUsername(username);
    List<Authenticator> auth = authRepository.findAllByUser(user);
    return auth.stream()
            .map(credential ->
                PublicKeyCredentialDescriptor.builder()
                    .id(credential.getCredentialId())
                    .build())
            .collect(Collectors.toSet());
}

The getUserHandleForUsername() function is called during credential authentication when the user provides a username. The authenticator looks up generated credentials with the help of the user handle byte array, which the server needs to store for this purpose. The demo application uses this function to look up the user handle that’s passed into the navigator.credential.get() function in the browser.

@Override
public Optional<ByteArray> getUserHandleForUsername(String username) {
    AppUser user = userRepo.findByUsername(username);
    return Optional.of(user.getHandle());
}

The getUsernameForUserHandle() function lets applications handle logins without usernames. The server organizes and recognizes users by means of usernames, but only user handles are necessary for the WebAuthn API to function. getUsernameForUserHandle can be used in login flows that don’t require usernames upfront. Hypothetically, an application could entirely dispense with usernames and identify users exclusively by unique byte array identifiers, though this application does not explore that possibility.

@Override
public Optional<String> getUsernameForUserHandle(ByteArray userHandle) {
    AppUser user = userRepo.findByHandle(userHandle);
    return Optional.of(user.getUsername());
}

The lookup() function is active during the final step of credential verification. A registered authenticator provides an assertion signature and a credential ID to the browser’s WebAuthn API, which is sent to the server. The server looks up a stored credential using the credential ID and user handle provided by the authenticator. At this point, the server will use the object returned by this lookup function to validate the assertion signature (and optionally the signature count) from the authenticator.

@Override
public Optional<RegisteredCredential> lookup(ByteArray credentialId, ByteArray userHandle) {
    Optional<Authenticator> auth = authRepository.findByCredentialId(credentialId);
    return auth.map(credential ->
        RegisteredCredential.builder()
            .credentialId(credential.getCredentialId())
            .userHandle(credential.getUser().getHandle())
            .publicKeyCose(credential.getPublicKey())
            .signatureCount(credential.getCount())
            .build()
    );
}

In a similar way, the lookupAll() function returns a set of RegisteredCredential objects. Instead of validating the authenticator’s signature, this function ensures that there aren’t multiple credentials registered with the same credential ID.

@Override
public Set<RegisteredCredential> lookupAll(ByteArray credentialId) {
    List<Authenticator> auth = authRepository.findAllByCredentialId(credentialId);
    return auth.stream()
            .map(credential ->
                RegisteredCredential.builder()
                    .credentialId(credential.getCredentialId())
                    .userHandle(credential.getUser().getHandle())
                    .publicKeyCose(credential.getPublicKey())
                    .signatureCount(credential.getCount())
                    .build())
            .collect(Collectors.toSet());
}

Yubico provides a RelyingParty object that’s responsible for handling all authentication requests on the server, so the application constructs and provides that object to all controllers. Because WebAuthn credentials are scoped to only one website, the hostname, origin, and a display name for the website are all provided by the relying party.

In the AppApplication class define the RelyingParty bean:

@Bean
public RelyingParty relyingParty(RegistrationRepository registrationRepository, 
                                 WebAuthProperties properties) {
    RelyingPartyIdentity rpIdentity = RelyingPartyIdentity.builder()
        .id(properties.getHostName())
        .name(properties.getDisplay())
        .build();

    return RelyingParty.builder()
        .identity(rpIdentity)
        .credentialRepository(registrationRepository)
        .origins(properties.getOrigin())
        .build();
}

Authentication controllers

The web controller for the application creates routes and constructs the data needed for the client to make WebAuthn API requests by:

  • Constructing JSON strings used in the registration and authentication ceremonies.
  • Caching intermediary information necessary for the secure execution of a ceremony (the challenge nonce).
  • Passing information from the browser’s credential creation ceremony to the data storage classes.

Create an AuthController class:

import com.yubico.webauthn.RelyingParty;
import org.springframework.stereotype.Controller;

@Controller
public class AuthController {

    private RelyingParty relyingParty;
    private RegistrationService service;

    AuthController(RegistrationService service, RelyingParty relyingPary) {
        this.relyingParty = relyingPary;
        this.service = service;
    }
    
    @GetMapping("/")
    public String welcome() {
        return "index";
    }
    
    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @GetMapping("/register")
    public String registerUser(Model model) {
        return "register";
    }
}

Both registration and authentication ceremonies happen in two steps. The client makes an initial request to the server with some identifier. The code below uses the username, but the user handle works as well. After prompting the user for a unique username, the server constructs a PublicKeyCredentialCreationOptions for registration, which contains the user handle, the domain of the key, and a challenge token.

Registration endpoints:

public String newUserRegistration(@RequestParam String username,
                                  @RequestParam String display,
                                  HttpSession session) {
    AppUser existingUser = service.getUserRepo().findByUsername(username);
    if (existingUser == null) {

        byte[] bytes = new byte[32];
        random.nextBytes(bytes);
        ByteArray id = new ByteArray(bytes);
        
        UserIdentity userIdentity = UserIdentity.builder()
                .name(username)
                .displayName(display)
                .id(id)
                .build();
        AppUser saveUser = new AppUser(userIdentity);
        service.getUserRepo().save(saveUser);
        String response = newAuthRegistration(saveUser, session);
        return response;
    } else {
        throw new ResponseStatusException(HttpStatus.CONFLICT, "Username " + username
            + " already exists. Choose a new name.");
    }
}

@PostMapping("/registerauth")
@ResponseBody
public String newAuthRegistration(@RequestParam AppUser user, HttpSession session) {
    AppUser existingUser = service.getUserRepo().findByHandle(user.getHandle());
    if (existingUser != null) {
        UserIdentity userIdentity = user.toUserIdentity();
        StartRegistrationOptions registrationOptions = StartRegistrationOptions.builder()
            .user(userIdentity)
            .build();
        PublicKeyCredentialCreationOptions registration = relyingParty.startRegistration(registrationOptions);
        session.setAttribute(userIdentity.getDisplayName(), registration);
        try {
            return registration.toCredentialsCreateJson();
        } catch (JsonProcessingException e) {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR,
                "Error processing JSON.", e);
        }
    } else {
        throw new ResponseStatusException(HttpStatus.CONFLICT, "User " + user.getUsername() 
            + " does not exist. Please register.");
    }
}

Authentication endpoints:

@PostMapping("/login")
@ResponseBody
public String startLogin(@RequestParam String username, HttpSession session) {
    AssertionRequest request = relyingParty.startAssertion(StartAssertionOptions.builder()
        .username(username)
        .build());
    try {
        session.setAttribute(username, request);
        return request.toCredentialsGetJson();
    } catch (JsonProcessingException e) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
    }
}

@PostMapping("/finishauth")
@ResponseBody
public ModelAndView finishRegisration(@RequestParam String credential,
                                      @RequestParam String username,
                                      @RequestParam String credname,
                                      HttpSession session) {
    try {
        AppUser user = service.getUserRepo().findByUsername(username);
        PublicKeyCredentialCreationOptions requestOptions = 
            (PublicKeyCredentialCreationOptions) session.getAttribute(user.getUsername());
        if (requestOptions != null) {
            PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
                PublicKeyCredential.parseRegistrationResponseJson(credential);
            FinishRegistrationOptions options = FinishRegistrationOptions.builder()
                .request(requestOptions)
                .response(pkc)
                .build();
            RegistrationResult result = relyingParty.finishRegistration(options);
            Authenticator savedAuth = new Authenticator(result, pkc.getResponse(), user, credname);
            service.getAuthRepository().save(savedAuth);
            return new ModelAndView("redirect:/login", HttpStatus.SEE_OTHER);
        } else {
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, 
                "Cached request expired. Try to register again!");
        }
    } catch (RegistrationFailedException e) {
        throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Registration failed.", e);
    } catch (IOException e) {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Failed to save credenital, please try again!", e);
    }
}

@PostMapping("/welcome")
public String finishLogin(@RequestParam String credential,
                          @RequestParam String username,
                          Model model,
                          HttpSession session) {
    try {
        PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc;
        pkc = PublicKeyCredential.parseAssertionResponseJson(credential);
        AssertionRequest request = (AssertionRequest)session.getAttribute(username);
        AssertionResult result = relyingParty.finishAssertion(FinishAssertionOptions.builder()
            .request(request)
            .response(pkc)
            .build());
        if (result.isSuccess()) {
            model.addAttribute("username", username);
            return "welcome";
        } else {
            return "index";
        }
    } catch (IOException e) {
        throw new RuntimeException("Authentication failed", e);
    } catch (AssertionFailedException e) {
        throw new RuntimeException("Authentication failed", e);
    }
}

NOTE: These requests require server state to work properly. The authentication request includes a challenge nonce generated as part of the StartRegistrationOptions and AssertionRequest objects. The reply from the client must include this nonce, so the server uses the session to cache the short-lived challenge by caching the full registration (or assertion) request object.

Information is sent to the browser, where it’s encoded and passed to the WebAuthn API as a PublicKeyCredential object. The response containing all the data needed for verification is sent to the server and used to create or verify a credential.

Start the registration ceremony

To kick off the registration ceremony, the client submits a uniquely identifying piece of information using a fetch request—in this case, a username. The server verifies that the username is unique and then creates and saves a 16-byte user handle. Configuration information about the server (like hostname and origin) was included in the RelyingParty bean when the server was created. This bean takes a PublicKeyCredentialRequestOptions object to collect the user information, and generates a challenge field, a random buffer of at least 16 bytes long. This object is cached on the server to preserve the challenge field, then turned into a JSON string and sent to the client.

IMPORTANT: You will need to grab the client code from the example’s GitHub repository. Copy the static and templates folders to your project’s src/main/resources directory.

# CSS
curl --create-dirs -O --output-dir src/main/resources/static/css https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/static/css/tachyons.css

# JS
curl --create-dirs -O --output-dir src/main/resources/static/javascript https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/static/javascript/base64js.min.js
curl --create-dirs -O --output-dir src/main/resources/static/javascript https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/static/javascript/custom.js
curl --create-dirs -O --output-dir src/main/resources/static/javascript https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/static/javascript/login.js
curl --create-dirs -O --output-dir src/main/resources/static/javascript  https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/static/javascript/registerauth.js

# Templates
curl --create-dirs -O --output-dir src/main/resources/templates https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/templates/index.html
curl --create-dirs -O --output-dir src/main/resources/templates https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/templates/register.html
curl --create-dirs -O --output-dir src/main/resources/templates https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/templates/login.html
curl --create-dirs -O --output-dir src/main/resources/templates https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/templates/welcome.html
curl --create-dirs -O --output-dir src/main/resources/templates https://raw.githubusercontent.com/oktadev/webauthn-java-example/main/src/main/resources/templates/header.html

Once the information is received by the client, it’s parsed into a JavaScript PublicKeyCredentialCreationOptions object. Then it is passed into the navigator.credentials.create() function as a publicKey option.

.then(credentialCreateJson => ({
    publicKey: {
        ...credentialCreateJson.publicKey,
        challenge: base64urlToUint8array(credentialCreateJson.publicKey.challenge),
    user: {
        ...credentialCreateJson.publicKey.user,
        id: base64urlToUint8array(credentialCreateJson.publicKey.user.id),
    },
    excludeCredentials: credentialCreateJson.publicKey.excludeCredentials.map(credential => ({
        ...credential,
        id: base64urlToUint8array(credential.id),
    })),
    extensions: credentialCreateJson.publicKey.extensions,
    },
}))
.then(credentialCreateOptions =>
    navigator.credentials.create(credentialCreateOptions))

The browser checks with whatever source of authentication is available to it using the “Client to Authenticator Protocol version two” (CTAP2). This check can be implemented by the operating system, browser, or by a standalone authentication device connected by USB or Bluetooth. This device generates a series of fields—the most important are credentialId and publicKey (generated from a private key the authenticator should never share). Next, the browser collates this information into a PublicKeyCredential, which is turned into a JSON object, stringified, and sent back to the server.

.then(publicKeyCredential => ({
    type: publicKeyCredential.type,
    id: publicKeyCredential.id,
    response: {
    attestationObject: uint8arrayToBase64url(publicKeyCredential.response.attestationObject),
    clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON),
    transports: publicKeyCredential.response.getTransports && publicKeyCredential.response.getTransports() || [],
    },
    clientExtensionResults: publicKeyCredential.getClientExtensionResults(),
}))
.then((encodedResult) => {
    const form = document.getElementById("form");
    const formData = new FormData(form);
    formData.append("credential", JSON.stringify(encodedResult));
    return fetch("/finishauth", {
        method: 'POST',
        body: formData
    })
})

The server receives the PublicKeyCredential object and verifies the challenge field against the cached PublicKeyCredentialCreationOptions object. It checks the origin of the response, then creates a new credential data object by saving the PublicKey and credentialId. The server also saves some helpful information like a user-friendly name, the associated user_id for username and user handle lookup, and the AAGUID field associated with the authenticator. In addition, it creates a count field for tracking the number of times the credential has been used. A simple application would probably only need to store publicKey and credentialId for the credential.

Register authentication frontend

Client JavaScript overview

The client is generating JavaScript objects containing byte fields that are Base64Url encoded. Don’t confuse this with regular base64 encoding. Base64Url is mostly the same, but it encodes - and _, whereas the more familiar base64 encodes + and \. This enables Base64Url to safely encode filenames and URLs. When encoding and decoding strings, be sure that the application is using the correct data encoding.

Also, be aware that the registration and authentication ceremonies performed by the client produce and consume byte fields in the form of Uint8Array. The demo application uses the base64js library to handle the encoding of these fields to and from Base64Url encoding to Uint8Array objects.

function base64urlToUint8array(base64Bytes) {
    const padding = '===='.substring(0, (4 - (base64Bytes.length % 4)) % 4);
    return base64js.toByteArray((base64Bytes + padding).replace(/\//g, "_").replace(/\+/g, "-"));
}

function uint8arrayToBase64url(bytes) {
    if (bytes instanceof Uint8Array) {
        return base64js.fromByteArray(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
    } else {
        return uint8arrayToBase64url(new Uint8Array(bytes));
    }
}

Authentication ceremony

Registering a credential is the first step in the authentication ceremony. Using a credential is similar, but the JSON objects provided to and produced by the client are slightly different. Additionally, the function the client calls is navigator.credentials.get() instead of create().

The client provides a username, which is sent to the server using a fetch request. This triggers the creation of an AssertionRequest request object, which includes an associated publicKey, a list of allowed credentialIds, and a generated challenge (creating the requirement that the AssertionRequest is cached server-side for security).

This AssertionRequest is passed to the client in a JSON response body. Any byte fields are again converted into Uint8Array objects, and the object is passed into navigator.credentials.get(). The resulting AuthenticatorAssertionResponse is Base64Url encoded and sent to the server as a JSON string. clientDataJson, like the server origin and challenge, are checked for tampering against the cached AssertionRequest; authenticatorData like signatureCount is updated and saved; and, most importantly, the signature field is checked against the stored publicKey generated during the creation ceremony. If all of these checks pass, the user is assigned a session, successfully signed in, and granted access to the application.

Sign-in with authentication frontend

Testing the application

To test this application, pull out any CTAP2-compliant authentication device. The easiest is activating WebAuthn emulation in Chrome. Using one of these devices, open the network inspector and watch the network requests to get a sense of the data passed back and forth between the client and server.

There are other devices that can be used for WebAuthn. For a complete list, you can do a search for FIDO-certified products:

  • Chromebook PIN
  • Windows Hello
  • YubiKey
  • Apple Touch ID

Start the application using your favorite IDE or run:

mvn spring-boot:run

Then open your browser to http://localhost:8080.

Successful authentication

An easier way to WebAuthn

Walking through this sample is a great way to learn how WebAuthn works in Java, but it’s still only part of an overall login experience. Your application needs a way to deal with other user scenarios, such as what to do when someone loses their YubiKey or drops their phone in a pool. Both Okta and Auth0 make it easy to add WebAuthn factor support to your application without applying any of the code above.

Why WebAuthn is the future

Associating devices with sign-ins provides a powerful tool to make many of the challenges, failures, and abuses of passwords a thing of the past. Once initial development is complete, user registration is simpler, more secure, and more sustainable. Using the WebAuthn specification, developers can create passwordless applications with an increasingly broad and accessible range of hardware authenticators.

Check out these posts for more information about WebAuthn:

If you have any questions about this post, please add a comment below. For more interesting content, follow @oktadev on Twitter, connect with us on LinkedIn, and subscribe to our YouTube channel.

John is a developer and writer working in Chicago. Failure and success both create growth, and growth never stops.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.