As discussed before, passkeys will refer to WebAuthn discoverable credentials. While this guide is focused on passkeys, it’s important to know how to implement a flow that supports non-discoverable credentials as they will be common amongst users, especially those using security keys.
What is the main difference between a discoverable and non-discoverable credential flow?
The primary difference is due to a user prompting the relying party to begin authentication by supplying a username. Passkey flows work in a generic fashion, they are not tailored to any individual user, and instead rely on a user passing in both a credential and user handle in order to identify themselves.
In a non-discoverable credential flow, a user starts by providing their user handle to a relying party. The relying party will then populate an allowCredentials
list - which notes credential IDs belonging to the provided user handle. This allowCredentials
list will only allow the client application to utilize credentials that match one of the provided IDs.
In fact, the allowCredentials
list is the primary difference between PublicKeyCredentialRequestOptions
that support discoverable credentials, and those for non-discoverable credentials. The PublicKeyCredentialRequestOptions
for passkeys will NOT include an allowCredentials
list.
So how can we modify our code in Figure 1 to support both flows? In fact it’s not difficult with the java-webauthn-server library. We modify our startAuthentication()
method to accept a user handle when invoked. No changes will need to be made to the finishAuthenitcation()
method.
Figure 6 will demonstrate a method that will allow for an authentication request to be invoked for both discoverable and non-discoverable credentials - Please note that some of the code leverages methods/class from the Yubico java-webauthn-server library.
Object startAuthentication(JsonObject jsonRequest) {
JsonElement jsonElement = jsonRequest.get("username");
Optional<String> username = Optional.ofNullable(jsonElement).map(JsonElement::getAsString);
if (username.isPresent() && !userStorage.userExists(username.get())) {
String msg = "The username \"" + username + "\" is not registered.";
return new Exception(msg);
} else {
AssertionRequest pubKey = rp.startAssertion(
StartAssertionOptions.builder()
.username(username)
.userVerification(UserVerificationRequirement.PREFERRED)
.build());
AssertionRequestWrapper request = new AssertionRequestWrapper(
generateRandom(32),
pubKey);
assertRequestStorage.put(request.getRequestId(), request);
String authRequestJson = gson.toJson(request, AssertionRequestWrapper.class);
return pubKey.toCredentialsGetJson();
}
}
You may notice that not much has changed in terms of what is included in the code. A quick rundown of the changes are as follows:
-
New method parameter that should include a field titled username
-
Attempt to read the username from the parameter object. We will utilize an Optional<String> value as a username will not be included in the case of a passkey flow
-
Next, if a username was presented, we will verify that the username has a credential registered in our credential repository
-
Lastly in our StartAssertionOptions.builder()
, we will add a step username()
and pass in our username as a value - Behind the scenes, if this value is null
then a PublicKeyCredentialRequestOptions
will be issued without an allowCredentials
list. If a username is provided, then the allowCredentials
list will be populated with the credential IDs belonging to the user
Lastly, ensure that the API that invokes the startAuthentication()
method has the ability for the user to add, or not include a username, to help support both authentication flows.
Next we will discuss how your relying party helps users manage their passkeys through credential management.