Skip to main content

Authentication flows

This section will cover the authentication flows for logging in with a passkey.

By the end of this section you will understand how to use and implement both of the /assertion/options and /assertion/result methods (as defined by our API)

Flow overview

The diagram below demonstrates how the relying party works with the client, and authenticator to authenticate with a passkey. When interacting with the relying party, the client will leverage both of the /assertion methods.

Docusaurus logo

The first call (/attestation/options) is used to receive an object that includes the options/configurations that should be used when creating a new credential.

The second call (/attestation/result) is used to send the newly created passkey to be stored in the relying party.

Assertion options method

This section will outline the assertion options method as well as provide a sample implementation in Java using the java-webauthn-server library.

API request and response schema

Request

Below is the request body of the /assertion/options method

{
"userName": "user@acme.com"
}

In this case, we are only signaling to the relying party the user which we are trying to authenticate. This method is implemented in a way that will support BOTH discoverable and non-discoverable credential flows. For the sake of this workshop, our focus will be on the discoverable credential flow, but we will outline how the non-discoverable credential flow is handled.

Discoverable credential flow

As you may recall from the fundamentals section, a WebAuthn credential must be discoverable in order to be a passkey. A discoverable credential refers to the ability for a relying party to attempt to utilize a credential on an authenticator without the user providing a user handle. This means that in our API we will signal to the RP that we wish to use a discoverable credential flow by passing in an empty username.

{
"userName": ""
}
Non-discoverable credential flow

In order to trigger the use of a non-discoverable credential flow, you will include the userName associated to the user who is trying to authenticate.

{
"userName": "user@acme.com"
}

Response

As indicated in the previous section, this method will include two different types of responses. In the WebAuthn specification, this object is referred to as the PublicKeyCredentialRequestOptions, but will contain extra data depending on the flow being used.

Discoverable credential flow

Below is the response body of the /attestation/options method for a discoverable credential flow

{
"requestId": "WlEPtNrJps-E03p_rwfXqASFkbrQ6Ml3Oy031JW4TYo",
"publicKey": {
"challenge": "NGc3jpB4Q-VnOmbhFBnDAczlYPT4soKA7xviGeJmDhc",
"timeout": 180000,
"rpId": "localhost",
"userVerification": "preferred"
}
}
Non-discoverable credential flow

Below is the response body of the /attestation/options method for a non-discoverable credential flow

{
"requestId": "B-J4odOi9vcV-4TN_gpokEb1f1EI...",
"publicKey": {
"challenge": "m7xl_TkTcCe0WcXI2M-4ro9vJAuwcj4m",
"timeout": 20000,
"rpId": "localhost",
"allowCredentials": [
{
"id": "opQf1WmYAa5aupUKJIQp",
"type": "public-key"
}
],
"userVerification": "preferred"
}
}

Note the major difference in both responses is the inclusion of the allowCredentials property, which only exists for non-discoverable credential flows. The id field in an allowCredentials entry relates to the credential ID of a WebAuthn credential that belongs to the user identified in the request body.

This means that the WebAuthn ceremony will only succeed if the user can demonstrate ownership of one of the credentials provided in the allowedCredentials list. The credential present in this list should be all of the credentials belonging to that user.

The exclusion of this list will allow the user to select a passkey (discoverable credential) on their authenticator, if one exists.

The key difference is that the non-discoverable flow is looking for a specific set of credentials, while the discoverable flow is looking for the user to select one of their credentials.

passkeys can be used in non-discoverable credential flows

Just because a passkey is a discoverable credential, does not mean that it cannot be used in a non-discoverable credential flow. Regardless of credential type, the RP will have the credential ID, and can be passed to the client if requested by the flow.

With that said, non-discoverable credentials cannot be leveraged in the discoverable credential flow. So if you have users who rely on the use of non-discoverable credentials, then ensure that you provide them with the option to utilize a non-discoverable credential flow

Implementation

Below is a sample implementation of the /attestation/options method. Note that request should correlate to the request body mentioned in the previous section, and response should correlate to the request response mentioned in the previous section.

public AssertionOptionsResponse assertionOptions(AssertionOptionsRequest request) throws Exception {
try {
/**
* Begin to build the PKC options
*/

StartAssertionOptionsBuilder optionsBuilder = StartAssertionOptions.builder();

// Configure the options with default values
optionsBuilder.userVerification(UserVerificationRequirement.PREFERRED).timeout(180000);

/**
* Check if the user has a credential stored in the DB
*/
Collection<CredentialRegistration> credentials = relyingPartyInstance.getStorageInstance().getCredentialStorage()
.getRegistrationsByUsername(request.getUserName());

/**
* If the user has no credentials, or the username was blank, then
* do not attempt to attach the allowCredentials property
*/
if (credentials.size() != 0 || request.getUserName() != "") {
/*
* To preserve privacy, if a request was made with a non-existent username, or
* missing a username
* then a discoverable credentials flow will be enabled
*/

optionsBuilder.username(request.getUserName());
}
AssertionRequest pkc = relyingPartyInstance.getRelyingParty()
.startAssertion(optionsBuilder.build());

ByteArray requestId = generateRandomByteArray(32);

/**
* Helper method to translate the pkc object into
* strings for use in the JSON request
*/
AssertionOptionsResponse response = AssertionOptionsResponseConverter
.PKCtoResponse(pkc.getPublicKeyCredentialRequestOptions(), requestId);

AssertionRequestStorage.insert(pkc, requestId.getBase64Url());

return response;
} catch (Exception e) {
e.printStackTrace();
throw new Exception("There was an issue while generating AssertionOptions: " + e.getMessage());
}
}

Assertion result method

This section will outline the assertion result method as well as provide a sample implementation in Java using the java-webauthn-server library.

API request and response schema

Request

Below is the request body of the /assertion/result method

{
"requestId": "B-J4odOi9vcV-4TN_gpokEb1f1EI...",
"assertionResult": {
"id": "LFdoCFJTyB82ZzSJUHc-c72yraRc_1mPvG",
"response": {
"authenticatorData": "SZYN5Gh0NBcPZHZgW4_krrmihjzzuoMdl2MBAAAAAA...",
"signature": "ME8fLjd5y6TUOLWt5l9DQIhANiYig9newAJZYTzG1i5lwP",
"userHandle": "string",
"clientDataJSON": "eyJjaGFTBrTmM4uIjoiaHR0cDovL2xvY2FsF1dGhuLmdldCJ9..."
},
"type": "public-key",
"clientExtensionResults": {}
}
}

These properties will include the result of the navigator.credentials.get method, along with the requestId, that was included in the /assertion/options response.

In most cases, you will directly pass the result of the navigator.credentials.get method into the assertionResult property.

Response

Below is the response body of the /assertion/result method

{
"status": "ok"
}

In this case, the result is simple. We include a property, status, that denotes the result of the authentication ceremony. If successful, then the status should be returned as ok, signaling to the caller that the authentication ceremony was successful. Otherwise, an error occurred and should be conveyed to the user.

Implementation

Below is a sample implementation of the /assertion/result method. Note that request should correlate to the request body mentioned in the previous section, and response should correlate to the request response mentioned in the previous section.

public AssertionResultResponse assertionResponse(AssertionResultRequest response) throws Exception {
try {
/**
* Check for assertion request
*/
Optional<AssertionOptions> maybeOptions = AssertionRequestStorage.getIfPresent(response.getRequestId());

AssertionOptions options;

if (maybeOptions.isPresent()) {
options = maybeOptions.get();
} else {
throw new Exception("Registration request not present");
}

/**
* Check if the request is still active
*/
if (!options.getIsActive()) {
// Not active, return error
throw new Exception("Registration request is no longer active");
} else {
AssertionRequestStorage.invalidate(response.getRequestId());
}

/**
* Build assertion request
*/
AssertionRequestBuilder requestBuilder = AssertionRequest.builder()
.publicKeyCredentialRequestOptions(options.getAssertionRequest().getPublicKeyCredentialRequestOptions());

// Check if a username was present in the request
if (options.getAssertionRequest().getUsername().isPresent()) {
requestBuilder.username(options.getAssertionRequest().getUsername().get());
}

AssertionResult result = relyingPartyInstance.getRelyingParty().finishAssertion(FinishAssertionOptions.builder()
.request(requestBuilder.build())
.response(parseAssertionResponse(response.getAssertionResult()))
.build());

if (result.isSuccess()) {
return AssertionResultResponse.builder().status("ok").build();
} else {
throw new Exception("Your assertion failed for an unknown reason");
}
} catch (Exception e) {
e.printStackTrace();
throw new Exception("There was an issue finalizing your assertion your credential: " + e.getMessage());
}
}