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.