Using this library comes in two parts: the server side and the client side.
The server side involves:
The client side involves:
-
Call navigator.credentials.create()
or .get()
,
passing the result from RelyingParty.startRegistration(...)
or .startAssertion(...)
as the argument.
-
Encode the result of the successfully resolved promise and return it to the server.
For this you need some way to encode Uint8Array
values;
this guide will use GitHub’s webauthn-json library.
Example code is given below.
For more detailed example usage, see
webauthn-server-demo
for a complete demo server.
1. Implement a CredentialRepository
2. Instantiate a RelyingParty
The
RelyingParty
class is the main entry point to the library.
You can instantiate it using its builder methods,
passing in your
CredentialRepository
implementation (called MyCredentialRepository
here) as an argument:
RelyingPartyIdentity rpIdentity = RelyingPartyIdentity.builder()
.id("example.com") // Set this to a parent domain that covers all subdomains
// where users' credentials should be valid
.name("Example Application")
.build();
RelyingParty rp = RelyingParty.builder()
.identity(rpIdentity)
.credentialRepository(new MyCredentialRepository())
.build();
3. Registration
A registration ceremony consists of 5 main steps:
This example uses GitHub’s webauthn-json library to do both (2) and (3) in one function call.
First, generate registration parameters and send them to the client:
Optional<UserIdentity> findExistingUser(String username) { /* ... */ }
PublicKeyCredentialCreationOptions request = rp.startRegistration(
StartRegistrationOptions.builder()
.user(
findExistingUser("alice")
.orElseGet(() -> {
byte[] userHandle = new byte[64];
random.nextBytes(userHandle);
return UserIdentity.builder()
.name("alice")
.displayName("Alice Hypothetical")
.id(new ByteArray(userHandle))
.build();
})
)
.build());
String credentialCreateJson = request.toCredentialsCreateJson();
return credentialCreateJson; // Send to client
Now call the WebAuthn API on the client side:
import * as webauthnJson from "@github/webauthn-json";
// Make the call that returns the credentialCreateJson above
const credentialCreateOptions = await fetch(/* ... */).then(resp => resp.json());
// Call WebAuthn ceremony using webauthn-json wrapper
const publicKeyCredential = await webauthnJson.create(credentialCreateOptions);
// Return encoded PublicKeyCredential to server
fetch(/* ... */, { body: JSON.stringify(publicKeyCredential) });
Validate the response on the server side:
String publicKeyCredentialJson = /* ... */; // publicKeyCredential from client
PublicKeyCredential<AuthenticatorAttestationResponse, ClientRegistrationExtensionOutputs> pkc =
PublicKeyCredential.parseRegistrationResponseJson(publicKeyCredentialJson);
try {
RegistrationResult result = rp.finishRegistration(FinishRegistrationOptions.builder()
.request(request) // The PublicKeyCredentialCreationOptions from startRegistration above
// NOTE: Must be stored in server memory or otherwise protected against tampering
.response(pkc)
.build());
} catch (RegistrationFailedException e) { /* ... */ }
Finally, if the previous step was successful, store the new credential in your database.
Here is an example of things you will likely want to store:
storeCredential( // Some database access method of your own design
"alice", // Username or other appropriate user identifier
result.getKeyId(), // Credential ID and transports for allowCredentials
result.getPublicKeyCose(), // Public key for verifying authentication signatures
result.getSignatureCount(), // Initial signature counter value
result.isDiscoverable(), // Is this a passkey?
result.isBackupEligible(), // Can this credential be backed up (synced)?
result.isBackedUp(), // Is this credential currently backed up?
pkc.getResponse().getAttestationObject(), // Store attestation object for future reference
pkc.getResponse().getClientDataJSON() // Store client data for re-verifying signature if needed
);
Like registration ceremonies, an authentication ceremony consists of 5 main steps:
This example uses GitHub’s webauthn-json library to do both (2) and (3) in one function call.
First, generate authentication parameters and send them to the client:
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username("alice") // Or .userHandle(ByteArray) if preferred
.build());
String credentialGetJson = request.toCredentialsGetJson();
return credentialGetJson; // Send to client
Now call the WebAuthn API on the client side:
import * as webauthnJson from "@github/webauthn-json";
// Make the call that returns the credentialGetJson above
const credentialGetOptions = await fetch(/* ... */).then(resp => resp.json());
// Call WebAuthn ceremony using webauthn-json wrapper
const publicKeyCredential = await webauthnJson.get(credentialGetOptions);
// Return encoded PublicKeyCredential to server
fetch(/* ... */, { body: JSON.stringify(publicKeyCredential) });
Validate the response on the server side:
String publicKeyCredentialJson = /* ... */; // publicKeyCredential from client
PublicKeyCredential<AuthenticatorAssertionResponse, ClientAssertionExtensionOutputs> pkc =
PublicKeyCredential.parseAssertionResponseJson(publicKeyCredentialJson);
try {
AssertionResult result = rp.finishAssertion(FinishAssertionOptions.builder()
.request(request) // The PublicKeyCredentialRequestOptions from startAssertion above
.response(pkc)
.build());
if (result.isSuccess()) {
return result.getUsername();
}
} catch (AssertionFailedException e) { /* ... */ }
throw new RuntimeException("Authentication failed");
Finally, if the previous step was successful, update your database using the
AssertionResult
.
Most importantly, you should update the signature counter. That might look something like this:
updateCredential( // Some database access method of your own design
"alice", // Query by username or other appropriate user identifier
result.getCredentialId(), // Query by credential ID of the credential used
result.getSignatureCount(), // Set new signature counter value
result.isBackedUp(), // Set new backup state flag
Clock.systemUTC().instant() // Set time of last use (now)
);
Then do whatever else you need - for example, initiate a user session.
5. Optional features: passkeys, multi-factor, backup state
WebAuthn supports a number of additional features beyond the basics:
A passkey is a WebAuthn credential that can simultaneously both identify and authenticate the user.
This is also called a discoverable credential.
By default, credentials are created non-discoverable, which means the server
must list them in the
allowCredentials
parameter before the user can use them to authenticate.
This is typically because the credential private key is not stored within the authenticator,
but instead encoded into one of the credential IDs in allowCredentials
.
This way even a small hardware authenticator can have an unlimited credential capacity,
but with the drawback that the user must first identify themself to the server
so the server can retrieve the correct allowCredentials
list.
Passkeys are instead stored within the authenticator, and also include the user’s
user handle
in addition to the credential ID.
This way the user can be both identified and authenticated simultaneously.
Many passkey-capable authenticators also offer a credential sync mechanism
to allow one passkey to be used on multiple devices.
PublicKeyCredentialCreationOptions request = rp.startRegistration(
StartRegistrationOptions.builder()
.user(/* ... */)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
.residentKey(ResidentKeyRequirement.REQUIRED)
.build())
.build());
The username can then be omitted when starting an authentication ceremony:
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder().build());
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username("alice")
.userVerification(UserVerificationRequirement.REQUIRED)
.build());
PublicKeyCredentialCreationOptions request = rp.startRegistration(
StartRegistrationOptions.builder()
.user(/* ... */)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.REQUIRED)
.build())
.build());
You can also request that user verification be used if possible, but is not required:
PublicKeyCredentialCreationOptions request = rp.startRegistration(
StartRegistrationOptions.builder()
.user(/* ... */)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.PREFERRED)
.build())
.build());
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username("alice")
.userVerification(UserVerificationRequirement.PREFERRED)
.build());
For example, you could prompt for a password as the second factor if isUserVerified()
returns false
:
AssertionResult result = rp.finishAssertion(/* ... */);
if (result.isSuccess()) {
if (result.isUserVerified()) {
return successfulLogin(result.getUsername());
} else {
return passwordRequired(result.getUsername());
}
}
User verification can be used with both discoverable credentials (passkeys) and non-discoverable credentials.
Passkeys on platform authenticators may also support the WebAuthn
autofill UI, also known as "conditional mediation".
This can help onboard users who are unfamiliar with a fully username-less login flow,
allowing a familiar username input field to opportunistically offer a shortcut using a passkey
if the user has one on their device.
This library is compatible with the autofill UI but provides no server-side options for it,
because the steps to enable it are taken on the front-end side.
Using autofill UI does not affect the response verification procedure.
See the guide on passkeys.dev
for complete instructions on how to enable the autofill UI.
In particular you need to:
-
Add the credential request option mediation: "conditional"
alongside the publicKey
option generated by
RelyingParty.startAssertion(...)
,
-
Add autocomplete="username webauthn"
to a username input field on the page, and
-
Call navigator.credentials.get()
in the background.
Because of technical limitations, autofill UI is as of May 2023 only supported for platform credentials,
i.e., passkeys stored on the user’s computing devices.
Autofill UI might support passkeys on external security keys in the future.
Some authenticators may allow credentials to be backed up and/or synced between devices.
This capability and its current state is signaled via the
Credential Backup State flags,
which are available via the isBackedUp()
and isBackupEligible()
methods of
RegistrationResult
and
AssertionResult
.
These can be used as a hint about how vulnerable a user is to authenticator loss.
In particular, a user with only one credential which is not backed up
may risk getting locked out if they lose their authenticator.