Server-side Web Authentication library for Java. Provides implementations of the Relying Party operations required for a server to support Web Authentication. This includes registering authenticators and authenticating registered authenticators.
Maven:
<dependency> <groupId>com.yubico</groupId> <artifactId>webauthn-server-core</artifactId> <version>1.11.0</version> <scope>compile</scope> </dependency>
Gradle:
compile 'com.yubico:webauthn-server-core:1.11.0'
This library uses semantic versioning.
The public API consists of all public classes, methods and fields in the com.yubico.webauthn
package and its subpackages,
i.e., everything covered by the
Javadoc.
Package-private classes and methods are NOT part of the public API.
The com.yubico:yubico-util
module is NOT part of the public API.
Breaking changes to these will NOT be reflected in version numbers.
In addition to the main webauthn-server-core
module, there are also:
-
webauthn-server-attestation
: A simple implementation of theMetadataService
interface, which by default comes preloaded with attestation metadata for Yubico devices. -
webauthn-server-core-minimal
: Alternative distribution ofwebauthn-server-core
, not dependent on BouncyCastle. Using it means you may have to add your own JCA providers to support some signature algorithms. In particular, OpenJDK 14 and earlier does not include providers for the EdDSA family of algorithms.
-
Generates request objects suitable as parameters to
navigator.credentials.create()
and.get()
-
Performs all necessary validation logic on the response from the client
-
No mutable state or side effects - everything (except builders) is thread safe
-
Optionally integrates with a "metadata service" to verify authenticator attestations and annotate responses with additional authenticator metadata
-
Reproducible builds: release signatures match fresh builds from source. See Reproducible builds below.
This library has no concept of accounts, sessions, permissions or identity federation, and it is not an authentication framework; it only deals with executing the WebAuthn authentication mechanism. Sessions, account management and other higher level concepts can make use of this authentication mechanism, but the authentication mechanism alone does not make a security system.
See the Javadoc for in-depth API documentation.
Using this library comes in two parts: the server side and the client side. The server side involves:
-
Implement the
CredentialRepository
interface with your database access logic. -
Instantiate the
RelyingParty
class. -
Use the
RelyingParty.startRegistration(...)
andRelyingParty.fininshRegistration(...)
methods to perform registration ceremonies. -
Use the
RelyingParty.startAssertion(...)
andRelyingParty.fininshAssertion(...)
methods to perform authentication ceremonies. -
Use the outputs of
finishRegistration
andfinishAssertion
to update your database, initiate sessions, etc.
The client side involves:
-
Call
navigator.credentials.create()
or.get()
, passing the result fromRelyingParty.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 assume use of base64-js. However the built-in parser methods forPublicKeyCredential
expect URL-safe Base64 encoding, so the below examples will include this as an additional step.
Example code is given below.
For more detailed example usage, see
webauthn-server-demo
for a complete demo server.
The CredentialRepository
interface
abstracts your database in a database-agnostic way.
The concrete implementation will be different for every project, but you can use
InMemoryRegistrationStorage
as a simple example.
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();
A registration ceremony consists of 5 main steps:
-
Generate registration parameters using
RelyingParty.startRegistration(...)
. -
Send registration parameters to the client and call
navigator.credentials.create()
. -
With
cred
as the result of the successfully resolved promise, callcred.getClientExtensionResults()
andcred.response.getTransports()
and return their results along withcred
to the server. -
Validate the response using
RelyingParty.finishRegistration(...)
. -
Update your database using the
finishRegistration
output.
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
You will need to keep this PublicKeyCredentialCreationOptions
object in temporary storage
so you can also pass it into finishRegistration(...)
later.
Now call the WebAuthn API on the client side:
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));
}
}
fetch(/* ... */) // Make the call that returns the credentialCreateJson above
.then(credentialCreateJson => ({ // Decode byte arrays from base64url
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),
})),
// Warning: Extension inputs could also contain binary data that needs encoding
extensions: credentialCreateJson.publicKey.extensions,
},
}))
.then(credentialCreateOptions => // Call WebAuthn ceremony
navigator.credentials.create(credentialCreateOptions))
.then(publicKeyCredential => ({ // Encode PublicKeyCredential for transport to server (example)
type: publicKeyCredential.type,
id: publicKeyCredential.id,
response: {
attestationObject: uint8arrayToBase64url(publicKeyCredential.response.attestationObject),
clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON),
// Attempt to read transports, but recover gracefully if not supported by the browser
transports: publicKeyCredential.response.getTransports && publicKeyCredential.response.getTransports() || [],
},
// Warning: Client extension results could also contain binary data that needs encoding
clientExtensionResults: publicKeyCredential.getClientExtensionResults(),
}))
.then(encodedResult =>
fetch(/* ... */)); // Return encoded PublicKeyCredential to server
Validate the response on the server side:
String publicKeyCredentialJson = /* ... */; // encodedResult 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.isDiscoverable(), // Can this key be used for username-less auth?
result.signatureCount(), // Initial signature counter value
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:
-
Generate authentication parameters using
RelyingParty.startAssertion(...)
. -
Send authentication parameters to the client, call
navigator.credentials.get()
and return the response. -
With
cred
as the result of the successfully resolved promise, callcred.getClientExtensionResults()
and return the result along withcred
to the server. -
Validate the response using
RelyingParty.finishAssertion(...)
. -
Update your database using the
finishAssertion
output, and act upon the result (for example, grant login access).
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
Again, you will need to keep this PublicKeyCredentialRequestOptions
object in temporary storage
so you can also pass it into finishAssertion(...)
later.
Now call the WebAuthn API on the client side:
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));
}
}
fetch(/* ... */) // Make the call that returns the credentialGetJson above
.then(credentialGetJson => ({ // Decode byte arrays from base64url
publicKey: {
...credentialGetJson.publicKey,
allowCredentials: credentialGetJson.publicKey.allowCredentials
&& credentialGetJson.publicKey.allowCredentials.map(credential => ({
...credential,
id: base64urlToUint8array(credential.id),
})),
challenge: base64urlToUint8array(credentialGetJson.publicKey.challenge),
// Warning: Extension inputs could also contain binary data that needs encoding
extensions: credentialGetJson.publicKey.extensions,
},
}))
.then(credentialGetOptions => // Call WebAuthn ceremony
navigator.credentials.get(credentialGetOptions))
.then(publicKeyCredential => ({ // Encode PublicKeyCredential for transport to server (example)
type: publicKeyCredential.type,
id: publicKeyCredential.id,
response: {
authenticatorData: uint8arrayToBase64url(publicKeyCredential.response.authenticatorData),
clientDataJSON: uint8arrayToBase64url(publicKeyCredential.response.clientDataJSON),
signature: uint8arrayToBase64url(publicKeyCredential.response.signature),
userHandle: publicKeyCredential.response.userHandle && uint8arrayToBase64url(publicKeyCredential.response.userHandle),
},
// Warning: Client extension results could also contain binary data that needs encoding
clientExtensionResults: publicKeyCredential.getClientExtensionResults(),
}))
.then(encodedResult =>
fetch(/* ... */)); // Return encoded PublicKeyCredential to server
Validate the response on the server side:
String publicKeyCredentialJson = /* ... */; // encodedResult 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.signatureCount(), // Set new signature counter value
Clock.systemUTC().instant() // Set time of last use (now)
);
Then do whatever else you need - for example, initiate a user session.
WebAuthn supports passwordless multi-factor authentication via on-authenticator user verification, and username-less authentication via discoverable credentials (sometimes the term "passwordless" is used to mean the combination of both, but here the two are treated separately).
Discoverable credentials must be enabled at registration time by setting the
authenticatorSelection
.residentKey
option:
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());
Some authenticators might enable this feature even if not required, and setting
the residentKey
option to ResidentKeyRequirement.PREFERRED
will enable it if the
authenticator supports it. The
credProps
extension
can be used to determine whether the created credential is discoverable, and is enabled by default.
User verification can be enforced independently per authentication ceremony:
AssertionRequest request = rp.startAssertion(StartAssertionOptions.builder()
.username("alice")
.userVerification(UserVerificationRequirement.REQUIRED)
.build());
Then RelyingParty.finishAssertion(...)
will enforce that user verification was
performed. However, there is no guarantee that the user’s authenticator will
support this unless the user has some credential created with the
authenticatorSelection
.userVerification
option set:
PublicKeyCredentialCreationOptions request = rp.startRegistration(
StartRegistrationOptions.builder()
.user(/* ... */)
.authenticatorSelection(AuthenticatorSelectionCriteria.builder()
.userVerification(UserVerificationRequirement.REQUIRED)
.build())
.build());
The library tries to place as few requirements on the overall application architecture as possible. For this reason it is stateless and free from side effects, and does not directly interact with any database. This means it is database agnostic and thread safe. The following diagram illustrates an example architecture for an application using the library.
The application manages all state and database access, and communicates with the library via POJO representations of requests and responses. The following diagram illustrates the data flow during a WebAuthn registration or authentication ceremony.
In this diagram, the Client is the user’s browser and the application’s client-side scripts. The Server is the application and its business logic, the Library is this library, and the Users database stores registered WebAuthn credentials.
-
The client requests to start the ceremony, for example by submitting a form. The
username
may or may not be known at this point. For example, the user might be requesting to create a new account, or we might be using username-less authentication. -
If the user does not already have a user handle, the application creates one in some application-specific way.
-
The application may choose to authenticate the user with a password or the like before proceeding.
-
The application calls one of the library’s "start" methods to generate a parameter object to be passed to
navigator.credentials.create()
or.get()
on the client. -
The library generates a random challenge and an assortment of other arguments depending on configuration set by the application.
-
If the
username
is known, the library uses a read-only database adapter provided by the application to look up the user’s credentials. -
The returned list of credential IDs is used to populate the
excludeCredentials
orallowCredentials
parameter. -
The library returns a
request
object which can be serialized to JSON and passed as thepublicKey
argument tonavigator.credentials.create()
or.get()
. For registration ceremonies this will be aPublicKeyCredentialCreationOptions
, and for authentication ceremonies it will be aPublicKeyCredentialRequestOptions
. The application stores therequest
in temporary storage. -
The application’s client-side script runs
navigator.credentials.create()
or.get()
withrequest
as thepublicKey
argument. -
The user confirms the operation and the client returns a
PublicKeyCredential
objectresponse
to the application. -
The application retrieves the
request
from temporary storage and passesrequest
andresponse
to one of the library’s "finish" methods to run the response validation logic. -
The library verifies that the
response
contents - challenge, origin, etc. - are valid. -
If this is an authentication ceremony, the library uses the database adapter to look up the public key for the credential named in
response.id
. -
The database adapter returns the public key.
-
The library verifies the authentication signature.
-
The library returns a POJO representation of the result of the ceremony. For registration ceremonies, this will include the credential ID and public key of the new credential. The application may opt in to also getting information about the authenticator model and whether the authenticator attestation is trusted. For authentication ceremonies, this will include the username and user handle, the credential ID of the credential used, and the new signature counter value for the credential.
-
The application inspects the result object and takes any appropriate actions as defined by its business logic.
-
If the result is not satisfactory, the application reports failure to the client.
-
If the result is satisfactory, the application proceeds with storing the new credential if this is a registration ceremony.
-
If this is an authentication ceremony, the application updates the signature counter stored in the database for the credential.
-
Finally, the application reports success and resumes its business logic.
Use the included
Gradle wrapper to
build the .jar
artifact:
$ ./gradlew :webauthn-server-core:jar
The output is built in the webauthn-server-core/build/libs/
directory, and the
version is derived from the most recent Git tag. Builds done on a tagged commit
will have a plain x.y.z
version number, while a build on any other commit will
result in a version number containing the abbreviated commit hash.
To run the tests:
$ ./gradlew check
To run the PIT mutation tests (this may take upwards of 30 minutes):
$ ./gradlew pitest
Starting in version 1.4.0-RC2
, artifacts are built reproducibly. Fresh builds from
tagged commits should therefore be verifiable by signatures from Maven Central
and GitHub releases:
$ git checkout 1.4.0-RC2
$ ./gradlew :webauthn-server-core:jar
$ wget https://repo1.maven.org/maven2/com/yubico/webauthn-server-core/1.4.0-RC2/webauthn-server-core-1.4.0-RC2.jar.asc
$ gpg --verify webauthn-server-core-1.4.0-RC2.jar.asc webauthn-server-core/build/libs/webauthn-server-core-1.4.0-RC2.jar
$ wget https://github.com/Yubico/java-webauthn-server/releases/download/1.4.0-RC2/webauthn-server-core-1.4.0-RC2.jar.asc
$ gpg --verify webauthn-server-core-1.4.0-RC2.jar.asc webauthn-server-core/build/libs/webauthn-server-core-1.4.0-RC2.jar
Note that building with a different JDK may produce a different artifact. To ensure binary reproducibility, please build with the same JDK as specified in the release notes. Reproducible builds also require building from a Git repository, since the build embeds version number and Git commit ID into the built artifacts.
Official Yubico software signing keys are listed on the Yubico Developers site.