1. Introduction
1.1. What is Web Authentication?
Web Authentication is a new, secure web application authentication specification standardized under W3C. By combining local authentication, public-key authentication, per-origin key management, it provides strong authentication to web sites against authentication process attacks like phishing. It is implemented in major browsers, offering an excellent choice for users who prioritize security and convenience.
1.2. What is Passkeys?
Passkeys is a user-friendly branding name for WebAuthn Level 3, designed to improve the usability of the WebAuthn specification.
1.3. WebAuthn4J
WebAuthn4J is a Java library for WebAuthn and Apple App Attest server side verification. It can be used not only for server-side verification of WebAuthn/Passkeys, but also for custom applications that use Apple App Attest or FIDO CTAP2 security keys. It is a portable library that supports all attestation statement formats defined in the Web Authentication specification while keeping external dependencies to a minimum.
1.4. Feature
1.4.1. Supported Attestation Statement Formats
All attestation statement formats are supported:
-
Packed attestation
-
FIDO U2F attestation
-
Android Key attestation
-
Android SafetyNet attestation
-
TPM attestation
-
Apple Anonymous attestation
-
Apple App Attest attestation
-
None attestation
1.4.2. Conformance
All mandatory test cases and optional Android Key attestation test cases of FIDO2 Test Tools provided by FIDO Alliance are passed.
| Since FIDO2 Test Tools runs the test via the REST API of FIDO2 Transport Binding Profile, it is executed through the REST API implementation provided by WebAuthn4j Spring Security. |
1.6. Getting from Maven Central
If you are using Maven, just add the webauthn4j as a dependency:
<properties>
...
<!-- Use the latest version whenever possible. -->
<webauthn4j.version>0.29.7.RELEASE</webauthn4j.version>
...
</properties>
<dependencies>
...
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
<version>${webauthn4j.version}</version>
</dependency>
...
</dependencies>
1.7. Source code
Source code for this project is hosted on Github.
git clone git@github.com:webauthn4j/webauthn4j.git
1.8. License
WebAuthn4J is an open source software licensed under Apache 2.0 License.
2. Quick Start
In this quick start guide, we’ll introduce an overview of the WebAuthn authentication process, and then details the scope, limitations, and how to implement WebAuthn authentication using the WebAuthn4J library.
2.1. Overview of WebAuthn Authentication process
2.1.1. Authentication Flow
In short, WebAuthn authentication is a public key-based authentication method designed for web applications.
First, a key pair is generated, with the public key stored on the server and the private key kept on the authenticator. At the time of authentication, the authenticator uses the private key to generate a signature, which is sent to the server to be verified with the public key, confirming the user’s identity.
The signed data comprises from the client data and the authenticator data. The client data contains not only the domain (origin) of the displayed site, but also the server-related information like a challenge generated beforehand by the server. The server not only verifies the signature but also confirms the challenge, helping to prevent replay attacks. Various other data and are also included in the signed client data and authenticator data, and the server verifies these as well. The data flow during authentication is illustrated in the diagram below.
2.1.2. Registration Flow
In the WebAuthn new credential registration process, the client asks the authenticator to generate a new key pair, and the public key and other data returned by the authenticator are registered on the server as credentials. Interestingly, the new credential registration follows a flow similar to authentication: the authenticator first signs the client data, including the challenge from the server and the authenticator data, then returns it. The server receives this via the client, verifies the signature, and, if successful, registers it as a credential record. For registration, however, the signed authenticator data also includes the newly generated public key, which is saved on the server and later used for signature verification in the authentication process.
The data flow during registration is illustrated in the diagram below.
As explained in the previous section, during authentication, the signature is created with the credential’s private key and verified with the credential’s public key. But then, what private key is used to sign the authenticator data and client data during the registration of the credential’s public key? And how does the server obtain the public key necessary for this signature verification? Typically, this private key is a unique key specific to each model of the authenticator and is embedded in the authenticator in advance. As for the public key used in verification, it can either be preconfigured on the server for trusted authenticator or obtained from a registry, such as the FIDO Metadata Service, which provides public keys for each authenticator model.
In this way, WebAuthn has a mechanism called "Attestation" that verifies that the authenticator being registered is of a specific model by signing the credential public key registration message with a model-specific private key. This data structure containing attestation information is called the "Attestation Statement". However, because the attestation statement reveals the model of the user’s authenticator, it could potentially be used for user tracking.
Therefore, in the default configuration, even if the authenticator returns an attestation statement, the client discards it and does not send it to the server. The attestation statement is only disclosed if explicitly specified as an option and with the end user’s consent.
2.2. Scope of WebAuthn4J
To achieve portability by not relying on any particular web application framework, WebAuthn4J intentionally narrows its functionality scope to server-side verification of WebAuthn registration and authentication.
For this reason, WebAuthn4J does not provide functions for retrieving parameters from HTTP requests, storing challenges in sessions, returning them to the frontend, saving generated public keys as credential records, or loading these records during authentication. These functions must be implemented according to the framework you are using.
If a WebAuthn4J wrapper library that manages these functions is available for your framework, it’s recommended to use it. For example:
-
Quarkus Security WebAuthn
-
Spring Security Passkeys
-
Vert.x Auth WebAuthn4J
If no wrapper library is available, you will need to implement these functions yourself, which will be explained in the next section.
2.3. Implementing the Registration Process Using WebAuthn4J
2.3.1. Generating a WebAuthn credential key pair
When calling the navigator.credentials.create method, various options can be specified. One of these options is challenge. As mentioned earlier, the challenge is a parameter used to prevent replay attacks; it should be generated by the server, passed as a parameter, and also saved in a session or similar storage.
According to the registration flow diagram, the backend server first generates the challenge, saves it in a session, and then sends it to the client.
The WebAuthn specification does not define a specific method for passing the challenge from the backend server to the frontend. You could embed it in an HTML page or set up a REST endpoint to return the challenge. Another good idea is to create an endpoint that returns the entire PublicKeyCredentialCreationOptions, a parameter for navigator.credentials.create. The WebAuthn JavaScript API provides a method called PublicKeyCredential.parseCreationOptionsFromJSON, which can parse a serialized JSON PublicKeyCredentialCreationOptions.
However, PublicKeyCredential.parseCreationOptionsFromJSON is not available on Safari versions earlier than 18.4.
For alternative solutions, refer to Alternative to Unsupported JSON Serialization APIs in Safari.
WebAuthn4J offers a Java class representing PublicKeyCredentialCreationOptions, which can be useful for assembling JSON on the backend server.
PublicKeyCredentialCreationOptions from the REST endpoint and calling navigator.credentials.createconst response = await fetch("/passkeys/attestationOptions") //fetch PublicKeyCredentialCreationOptions as JSON string
const publicKeyCredentialCreationOptionsJSON = await response.json() // convert to JSONObject
const credentialCreationOptions = PublicKeyCredential.parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // convert to PublicKeyCredentialCreationOptions
const publicKeyCredential = await navigator.credentials.create({ publicKey: credentialCreationOptions}); // create PublicKeyCredential
In any case, generate the challenge on the backend server, store it in the session, and pass it to the frontend by some means.
Then, in the frontend JavaScript, call the navigator.credentials.create method with it to generate the WebAuthn credential. For more information on the other options available for the navigator.credentials.create method, please refer MDN: CredentialsContainer: create() method.
2.3.2. Registering the WebAuthn public key credential on the server
The generated WebAuthn credential must be sent to the backend server in some way.
The WebAuthn specification does not define the format in which it should be sent to the server.
However, the JavaScript type PublicKeyCredential, representing a WebAuthn credential, has a toJSON method.
Using this method along with JSON.stringify to serialize the data is considered a best practice for transmission.
However, this toJSON method is also not available in Safari.
For alternative solutions, refer to Alternative to Unsupported JSON Serialization APIs in Safari.
PublicKeyCredentialconst registrationResponseJSON = publicKeyCredential.toJSON(); // convert to JSONObject
await fetch("/register", {
method : 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
'username': document.getElementById('username').value,
'registrationResponseJSON': JSON.stringify(registrationResponseJSON) //convert to string
})
});
The backend server needs to verify the received WebAuthn credential and then persist the WebAuthn credential record, which includes the public key.
With WebAuthn4J, you can directly verify the JSON representation of PublicKeyCredential using the WebAuthnManager#verifyRegistrationResponseJSON method. The WebAuthnManager#parseRegistrationResponseJSON method only performs deserialization of PublicKeyCredential without verification.
If you want to access the parsed data when an error occurs during verification, parse it with WebAuthnManager#parseRegistrationResponseJSON to obtain an instance of RegistrationData, then pass it to the WebAuthnManager#verify method for verification.
PublicKeyCredentialString registrationResponseJSON = "<registrationResponseJSON>"; /* set registrationResponseJSON received from frontend */
RegistrationData registrationData;
try {
registrationData = webAuthnManager.parseRegistrationResponseJSON(registrationResponseJSON);
}
catch (DataConversionException e) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e;
}
// Server properties
Origin origin = null /* set origin */;
String rpId = null /* set rpId */;
Challenge challenge = null /* set challenge */;
ServerProperty serverProperty = ServerProperty.builder()
.origin(origin)
.rpId(rpId)
.challenge(challenge)
.build();
// expectations
List<PublicKeyCredentialParameters> pubKeyCredParams = null;
boolean userVerificationRequired = false;
boolean userPresenceRequired = true;
RegistrationParameters registrationParameters = new RegistrationParameters(serverProperty, pubKeyCredParams, userVerificationRequired, userPresenceRequired);
try {
webAuthnManager.verify(registrationData, registrationParameters);
} catch (VerificationException e) {
// If you would like to handle WebAuthn data verification error, please catch VerificationException
throw e;
}
// please persist CredentialRecord object, which will be used in the authentication process.
CredentialRecord credentialRecord =
new CredentialRecordImpl( // You may create your own CredentialRecord implementation to save friendly authenticator name
registrationData.getAttestationObject(),
registrationData.getCollectedClientData(),
registrationData.getClientExtensions(),
registrationData.getTransports()
);
save(credentialRecord); // please persist credentialRecord in your manner
RegistrationParameters is another argument of the WebAuthnManager#verifyRegistrationResponseJSON method, containing parameters that encapsulate the server state and verification conditions.
-
serverProperty: A parameter that conveys the server state. See ServerProperty for details. -
pubKeyCredParams: Specify the same value as thepubKeyCredParamsprovided inPublicKeyCredentialCreationOptions. -
userVerificationRequired: A parameter indicating whether user verification, such as biometrics or PIN confirmation on the authenticator, is required. -
userPresenceRequired: A parameter specifying whether the user’s presence verification on the authenticator is mandatory. This verifies theUPflag, which indicates that the user performed some gesture input. This gesture could be something like a touch on a capacitive button, not limited to biometric authentication. In WebAuthn, theUPflag is generally required, so it should be set totrue, except in scenarios that auto-generating credentials during a password-to-passkey upgrade, wherefalseis required.
2.3.3. ServerProperty
The server state is encapsulated in serverProperty. When creating the ServerProperty (via the builder), specify the following values:
-
For
origin, specify the origin you expect the client browser (user agent) to have recognized at the time of the WebAuthn registration and authentication ceremony. The browser writes the recognized origin to clientData and includes this content in the signature. On the server side, by verifying that the origin recorded in clientData matches the expected value, you can prevent ceremonies that are fraudulently launched from unexpected pages or other origins (phishing and framing attacks). -
For
rpId, specify the rpId of the site providing WebAuthn authentication. rpId is a parameter that specifies the scope of the credentials. For details, see the rpId section of the WebAuthn specification. -
For
challenge, specify the issued challenge.challengeis a parameter to prevent replay attacks. You can protect users from replay attacks by generating a random byte sequence as achallengeon the server side, specifying it as a parameter when executing the WebAuthn JS API on the front-end, including it in the signature, and verifying that the values match on the server side. It is the responsibility of the WebAuthn4J library caller to persist the issued challenge until verification. It is a good idea to store it in the session or somewhere similar. -
(Optional) You can specify the top origins to allow by using
topOrigin(…)/topOrigins(…)/topOriginPredicate(…).-
WebAuthn does not allow cross-origin access in principle, due to the same-origin constraint and clickjacking countermeasures.
-
However, in use cases such as embedding an authentication widget on a site with a different origin, you may want to allow access only from specific parent pages. In that case, please explicitly specify the parent pages (origin of the top-level document) you want to allow using
topOrigin(…)/topOrigins(…)/topOriginPredicate(…). If you absolutely need to allow any parent page, you can useanyTopOrigin(), but this should be used with caution as it increases the attack surface. -
Example (Allow only specific parent pages):
ServerProperty serverProperty = ServerProperty.builder() .origin(new Origin("https://rp.example.com")) .rpId("rp.example.com") .challenge(challenge) .topOrigin(new Origin("https://parent.example.com")) .build(); -
Example (Allow/Disable any parent page):
ServerProperty serverProperty = ServerProperty.builder() .origin(new Origin("https://rp.example.com")) .rpId("rp.example.com") .challenge(challenge) .anyTopOrigin() .build();
-
If verification succeeds, create a CredentialRecord instance from the returned values and persist it in a database or similar storage for authentication.
For more information on persistence methods, see Credential Record serialization and deserialization.
If verification fails, a subclass of VerificationException will be thrown.
2.4. Implementing the Authentication Process Using WebAuthn4J
2.4.1. Generating a WebAuthn Assertion
The primary API used during WebAuthn authentication is the browser’s navigator.credentials.get method. As illustrated in the authentication flow diagram, first the backend server needs to generate a challenge, save it in a session, and pass it to the client.
This is necessary because the navigator.credentials.get method requires a challenge parameter.
The WebAuthn specification does not define a specific method for transferring the challenge from the backend server to the frontend (client) for authentication.
Just as with the registration process, feel free to use any preferred method to pass the challenge to the frontend. The JavaScript API for parsing PublicKeyCredentialRequestOptions, a parameter of navigator.credentials.get, is PublicKeyCredential.parseCreationGetOptionsFromJSON.
For alternative solutions to the issue that PublicKeyCredential.parseCreationGetOptionsFromJSON is not available in Safari, refer to Alternative to Unsupported JSON Serialization APIs in Safari.
For additional options that can be specified for the navigator.credentials.get method, please refer MDN: CredentialsContainer: get() method.
PublicKeyCredentialRequestOptions from the REST endpoint and callingnavigator.credentials.get
const response = await fetch("/passkeys/assertionOptions");
const publicKeyCredentialRequestOptionsJSON = await response.json();
const credentialGetOptions = PublicKeyCredential.parseRequestOptionsFromJSON(publicKeyCredentialRequestOptionsJSON);
const publicKeyCredential = await navigator.credentials.get({ publicKey: credentialGetOptions});
2.4.2. WebAuthn Assertion Verification and Post-Processing
The assertion generated by the navigator.credentials.get method needs to be sent to the backend server for verification.
As with the registration, it can be serialized using the toJSON method.
PublicKeyCredentialconst authenticationResponseJSON = publicKeyCredential.toJSON();
console.debug("authenticationResponseJSON: %s", authenticationResponseJSON);
await fetch("/passkeys/authenticate", {
method : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(authenticationResponseJSON)
});
With WebAuthn4J, you can verify the JSON representation of PublicKeyCredential using the WebAuthnManager#verifyAuthenticationResponseJSON method.
If you wish to perform parsing and verification as two separate steps, use the WebAuthnManager#parseAuthenticationResponseJSON and WebAuthnManager#verify methods.
PublicKeyCredentialString authenticationResponseJSON = "<authenticationResponseJSON>"; /* set authenticationResponseJSON received from frontend */
AuthenticationData authenticationData;
try {
authenticationData = webAuthnManager.parseAuthenticationResponseJSON(authenticationResponseJSON);
} catch (DataConversionException e) {
// If you would like to handle WebAuthn data structure parse error, please catch DataConversionException
throw e;
}
// Server properties
Origin origin = null /* set origin */;
String rpId = null /* set rpId */;
Challenge challenge = null /* set challenge */;
ServerProperty serverProperty = ServerProperty.builder()
.origin(origin)
.rpId(rpId)
.challenge(challenge)
.build();
// expectations
List<byte[]> allowCredentials = null;
boolean userVerificationRequired = true;
boolean userPresenceRequired = true;
CredentialRecord credentialRecord = load(authenticationData.getCredentialId()); // please load authenticator object persisted in the registration process in your manner
AuthenticationParameters authenticationParameters =
new AuthenticationParameters(
serverProperty,
credentialRecord,
allowCredentials,
userVerificationRequired,
userPresenceRequired
);
try {
webAuthnManager.verify(authenticationData, authenticationParameters);
} catch (VerificationException e) {
// If you would like to handle WebAuthn data validation error, please catch ValidationException
throw e;
}
// please update the counter of the authenticator record
updateCounter(
authenticationData.getCredentialId(),
authenticationData.getAuthenticatorData().getSignCount()
);
Note: Origin verification checks that the Origin recognized by the browser at the time of the ceremony matches your expected Origin. Cross‑origin iframe execution is disallowed by default. For legitimate embedding use cases, explicitly configure server‑side acceptance of specific parent origins via the ServerProperty builder (topOrigin(…)/topOrigins(…)/topOriginPredicate(…)), or use anyTopOrigin() only if strictly necessary. The client’s crossOrigin value is a supplemental signal; the server’s top‑origin configuration determines allow/deny.
The AuthenticationParameters, which is another argument of the WebAuthnManager#verifyAuthenticationResponseJSON method, is a parameter that encapsulates the server’s state and verification conditions.
-
serverProperty: A parameter that conveys the server’s state. For more information, refer to ServerProperty. -
userVerificationRequired: A parameter specifies whether user verification, like biometric authentication or PIN confirmation on the authenticator is required. For multi-step authentication involving a password and device possession, this can be set tofalse, as the password confirms knowledge factor. For password-less authentication, this should be set totrue. -
authenticator: Specify theCredentialRecordthat was persisted during registration.
If verification succeeds, the authentication is considered successful, and the counter,
uvInitialized, and backedUp values linked to the persisted CredentialRecord should be updated.
The counter is used to detect cloning of the authenticator. For details on counters, see the counter section of the WebAuthn specification.
Then, complete any necessary steps for successful user authentication, such as creating an authenticated session.
If verification fails, a subclass of VerificationException will be thrown.
2.5. Apple App Attest verification
Next, how to verify Apple App Attest is explained. Since Apple App Attest has a data structure similar to WebAuthn, the validator design follows that of WebAuthn. Risk metric evaluation is not supported for now.
2.5.1. Getting from Maven Central
Apple App Attest validators are contained in the dedicated webauthn4j-appattest module.
If you are using maven, add the webauthn4j-appattest as a dependency in this way:
<properties>
...
<!-- Use the latest version whenever possible. -->
<webauthn4j.version>0.29.7.RELEASE</webauthn4j.version>
...
</properties>
<dependencies>
...
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-appattest</artifactId>
<version>${webauthn4j.version}</version>
</dependency>
...
</dependencies>
2.5.2. Apple App Attest attestation verification
To verify an attestation on authenticator registration, call DeviceCheckManager#verify with a
DCAttestationRequest instance as an argument.
If you would like to access the parsed data when an validation error occurred, please use DeviceCheckManager#parse to parse the attestation request and pass the returned DCAttestationData instance to DeviceCheckManager#verify
method.
The members of DCAttestationRequest are the values obtained by the Apple App Attest API in the iOS device Transmit from the iOS device to the server side in some way.
DCAttestationParameters is an another argument for DeviceCheckManager#parse method, and contains server property and validation conditions.
DCServerProperty has following members.
-
For
teamIdentifier, please set the teamIdentifier used for your iOS App development. For more details, please refer to Validating Apps that connect to your server. -
For
cfBundleIdentifier, please set the cfBundleIdentifier used for your iOS App development. For more details, please refer to Validating Apps that connect to your server. -
For
challenge, please specify the Challenge issued on App Attest API call.challengeis a parameter to prevent replay attacks. By issuing the random byte sequencechallengeon server side, signing it with App Attest API, and verifying the signature on server side, users are protected from the replay attack. It is the application’s responsibility for retaining the issued Challenge.
If validation fails, an exception inheriting VerificationException is thrown.
If validation succeeds, please create an DCAppleDevice instance from the returned value and persist it to the database or something in your application manner.
The instance is required at the time of authentication.
Production environment? Development environment?
Apple App Attest can return a development attestation for development.
By default, webAuthn4j-appattest is set to accept a production attestation.
If you want to accept a development attestation, you need to DCAttestationDataVerifier#setProduction false.
// Client properties
byte[] keyId = null; /* set keyId */
byte[] attestationObject = null; /* set attestationObject */
byte[] challenge = null; /* set challenge */
byte[] clientDataHash = MessageDigestUtil.createSHA256().digest(challenge);
// Server properties
String teamIdentifier = null /* set teamIdentifier */;
String cfBundleIdentifier = null /* set cfBundleIdentifier */;
DCServerProperty dcServerProperty = new DCServerProperty(teamIdentifier, cfBundleIdentifier, new DefaultChallenge(challenge));
DCAttestationRequest dcAttestationRequest = new DCAttestationRequest(keyId, attestationObject, clientDataHash);
DCAttestationParameters dcAttestationParameters = new DCAttestationParameters(dcServerProperty);
DCAttestationData dcAttestationData;
try {
dcAttestationData = deviceCheckManager.parse(dcAttestationRequest);
} catch (DataConversionException e) {
// If you would like to handle Apple App Attest data structure parse error, please catch DataConversionException
throw e;
}
try {
deviceCheckManager.verify(dcAttestationData, dcAttestationParameters);
} catch (VerificationException e) {
// If you would like to handle Apple App Attest data validation error, please catch VerificationException
throw e;
}
// please persist Authenticator object, which will be used in the authentication process.
DCAppleDevice dcAppleDevice =
new DCAppleDeviceImpl( // You may create your own Authenticator implementation to save friendly authenticator name
dcAttestationData.getAttestationObject().getAuthenticatorData().getAttestedCredentialData(),
dcAttestationData.getAttestationObject().getAttestationStatement(),
dcAttestationData.getAttestationObject().getAuthenticatorData().getSignCount(),
dcAttestationData.getAttestationObject().getAuthenticatorData().getExtensions()
);
save(dcAppleDevice); // please persist authenticator in your manner
2.5.3. Apple App Attest assertion verification
To parse and verify an assertion on authentication, call DeviceCheckManager#verify with a DCAssertionRequest
instance as an argument.
If you would like to access the parsed data when an validation error occurred, please use
DeviceCheckManager#parse to parse the authentication request and pass the returned DCAssertionData instance to DeviceCheckManager#verify method.
The members of DCAssertionRequest are the values obtained by the App Attest API in the iOS device.
Transmit from the iOS device to the server side in some way.
DCAssertionParameters is an another argument for DeviceCheckManager#parse method, and contains server property, persisted authenticator and validation conditions.
// Client properties
byte[] keyId = null /* set keyId */;
byte[] assertion = null /* set assertion */;
byte[] clientDataHash = null /* set clientDataHash */;
// Server properties
String teamIdentifier = null /* set teamIdentifier */;
String cfBundleIdentifier = null /* set cfBundleIdentifier */;
byte[] challenge = null;
DCServerProperty dcServerProperty = new DCServerProperty(teamIdentifier, cfBundleIdentifier, new DefaultChallenge(challenge));
DCAppleDevice dcAppleDevice = load(keyId); // please load authenticator object persisted in the attestation process in your manner
DCAssertionRequest dcAssertionRequest =
new DCAssertionRequest(
keyId,
assertion,
clientDataHash
);
DCAssertionParameters dcAssertionParameters =
new DCAssertionParameters(
dcServerProperty,
dcAppleDevice
);
DCAssertionData dcAssertionData;
try {
dcAssertionData = deviceCheckManager.parse(dcAssertionRequest);
} catch (DataConversionException e) {
// If you would like to handle Apple App Attest data structure parse error, please catch DataConversionException
throw e;
}
try {
deviceCheckManager.verify(dcAssertionData, dcAssertionParameters);
} catch (ValidationException e) {
// If you would like to handle Apple App Attest data validation error, please catch ValidationException
throw e;
}
// please update the counter of the authenticator record
updateCounter(
dcAssertionData.getCredentialId(),
dcAssertionData.getAuthenticatorData().getSignCount()
);
3. Configuration
WebAuthn4J has a one main entry point class, WebAuthnManager.
It delegates attestation statements verification to an implementation of
AttestationStatementVerifier and attestation statements trustworthiness verification to an implementation of
CertPathTrustworthinessVerifier.
Since most sites don’t require strict attestation statement verification (WebAuthn Spec related topic ), WebAuthn4J provides WebAuthnManager.createNonStrictWebAuthnManager factory method that returns an WebAuthnManager instance configured AttestationStatementVerifier and
CertPathTrustworthinessVerifier not to verify attestation statements.
If you are engaging an enterprise use case and strict authenticator verification is a requirement, Use the constructor of the WebAuthnManager class and inject verifiers.
3.1. Attestation statement verification
Attestation statement verification is provided by the implementation of AttestationStatementVerifier interface.
For each attestation statement format, corresponding verifier classes are provided.
Please specify its list at the first argument of the constructor of WebAuthnManager class.
For example, if you would like to limit the supported format to packed only, add only
PackedAttestationStatementVerifier to the List, and if you would like to support packed and tpm format, make the List with PackedAttestationStatementVerifier and TPMAttestationStatementVerifier.
Do NOT combine no-op verifiers like NoneAttestationStatementVerifier or NullPackedAttestationStatementVerifier with other AttestationStatementVerifier s.
Mixing non-verifying verifiers, with verifying verifiers will create a vulnerability that can be exploited to bypass attestation verification.
3.1.1. Attestation statement trustworthiness verification
Attestation statement trustworthiness verification has two patterns: certificate path verification, and self attestation.
Certificate path verification is delegated via CertPathTrustworthinessVerifier interface.
WebAuthn4J provides DefaultCertPathTrustworthinessVerifier as CertPathTrustworthinessVerifier implementation.
DefaultCertPathTrustworthinessVerifier verifies trustworthiness by checking the attestation certificate chains to the root certificate provided as TrustAnchor via TrustAnchorRepository interface.
3.1.2. Trust anchor resolution
TrustAnchorRepository is an interface that resolves TrustAnchor from AAGUID or attestationCertificateKeyIdentifier.
webauthn4j-core module provides a KeyStoreTrustAnchorRepository as a TrustAnchorRepository.
KeyStoreTrustAnchorRepository fetches TrustAnchor from a Java Key Store. Please note that
KeyStoreTrustAnchorRepository does not return a different TrustAnchor depending on AAGUID or attestationCertificateKeyIdentifier.
All certificates registered in the Java Key Store file are treated as trust anchors.
Trust anchor resolution using FIDO Metadata Service
webauthn4j-metadata module, which provides FIDO Metadata Statement handling, is under experimental status.
|
FIDO Alliance offers FIDO Metadata Service, which provides metadata of authenticators.
webauthn4j-metadata module provides a MetadataBLOBBasedTrustAnchorRepository as a TrustAnchorRepository implementation.
MetadataBLOBBasedTrustAnchorRepository can provide trust anchors based on the information published by FIDO Metadata Service when it is used in combination with FidoMDS3MetadataBLOBAsyncProvider.
4. Deep-Dive
4.1. Representation of a credential record
WebAuthn4j provides CredentialRecord interface as a representation of a credential record.
On registering the credential, you need to persist its representation by creating the instance implementing
CredentialRecord interface in your application manner because it is used afterwards on authentication verification.
It might be better to use credentialId as a search key for this persisted instance.
You can freely enhance the class implementing CredentialRecord interface in order to meet your application’s requirements.
For example, you can add a field like name to identify the credential.
4.2. CredentialRecord serialization and deserialization
While it is application’s responsibility to serialize CredentialRecord instance at registration, WebAuthn4J provides an utility class to serialize or deserialize fields of CredentialRecord class.
Please use them for implementing persistence in your application.
4.2.1. attestedCredentialData
AttestedCredentialDataConverter converts from AttestedCredentialData to byte[] and vice versa.
If you would like to persist as String, use Base64UrlUtil to convert from byte[] to base64url String.
AttestedCredentialDataConverter attestedCredentialDataConverter = new AttestedCredentialDataConverter(objectConverter);
// serialize
byte[] serialized = attestedCredentialDataConverter.convert(attestedCredentialData);
// deserialize
AttestedCredentialData deserialized = attestedCredentialDataConverter.convert(serialized);
4.2.2. attestationStatement
Since AttestationStatement is an interface, there are some implementation classes like PackedAttestationStatement or AndroidKeyAttestationStatement per format.
As AttestationStatement is not self-descriptive for its format, the format need to be persisted in an another field.
Because of that, an envelope class which has attestationStatement field and format field is required, and the envelope class need to be serialized for persisting AttestationStatement.
Since the envelope class itself is not provided by the WebAuthn4J library, please implement your own envelope class on the application side, referring to the example below.
//serialize
AttestationStatementEnvelope envelope = new AttestationStatementEnvelope(attestationStatement);
byte[] serializedEnvelope = objectConverter.getCborConverter().writeValueAsBytes(envelope);
//deserialize
AttestationStatementEnvelope deserializedEnvelope = objectConverter.getCborConverter().readValue(serializedEnvelope, AttestationStatementEnvelope.class);
AttestationStatement deserializedAttestationStatement = deserializedEnvelope.getAttestationStatement();
class AttestationStatementEnvelope{
@JsonProperty("attStmt")
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "fmt"
)
private AttestationStatement attestationStatement;
@JsonCreator
public AttestationStatementEnvelope(@JsonProperty("attStmt") AttestationStatement attestationStatement) {
this.attestationStatement = attestationStatement;
}
@JsonProperty("fmt")
public String getFormat() {
return attestationStatement.getFormat();
}
public AttestationStatement getAttestationStatement() {
return attestationStatement;
}
}
4.2.3. transports
If you would like to persist as JSON String, use ObjectConverter.
String serializedTransports = objectConverter.getJsonConverter().writeValueAsString(transports);
4.2.5. authenticatorExtensions
This member can be serialized as CBOR bytes array as it is originally CBOR data.
If you would like to persist as String, use Base64UrlUtil to convert from byte[] to base64url String.
byte[] serializedAuthenticatorExtensions = objectConverter.getCborConverter().writeValueAsBytes(authenticatorExtensions);
4.3. DCAppleDevice serialization and deserialization
When you use webauthn4j-appattest, you need to persist DCAppleDevice instead of CredentialRecord interface between attestation and assertion.
In general, you can serialize and deserialize it by the method explained in CredentialRecord serialization and deserialization, but ObjectConverter must be the one with DeviceCheckCBORModule registered.
A ObjectConverter with a DeviceCheckCBORModule can be obtained with DeviceCheckManager.createObjectConverter static method.
4.4. Alternative to Unsupported JSON Serialization APIs in Safari
In the Quick Start, PublicKeyCredential.parseCreationOptionsFromJSON is introduced as an API for parsing PublicKeyCredentialCreationOptions, and PublicKeyCredential#toJSON as an API for serializing PublicKeyCredential. However, these APIs are not available on Safari versions earlier than 18.4.
As an alternative, it is recommended to use the pony-fill provided by the npm library github/@webauthn-json, maintained by GitHub.
It provides parseCreationOptionsFromJSON method as a substitute for PublicKeyCredential.parseCreationOptionsFromJSON and create as a substitute for navigator.credentials.create.
import {
create,
parseCreationOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialCreationOptions as JSON>") //fetch PublicKeyCredentialCreationOptions as JSON string
const publicKeyCredentialCreationOptionsJSON = await response.json() // convert to JSONObject
const credentialCreationOptions = parseCreationOptionsFromJSON(publicKeyCredentialCreationOptionsJSON); // convert to PublicKeyCredentialCreationOptions
const publicKeyCredential = await create({ publicKey: credentialCreationOptions}); // create PublicKeyCredential
const registrationResponseJSON = publicKeyCredential.toJSON() // JSON object of publicKeyCredential
const registrationResponseJSONStr = JSON.stringify(registrationResponseJSON) // JSON string representation of publicKeyCredential
The toJSON method can be used on the publicKeyCredential obtained using the create method of this pony-fill.
It also provides parseRequestOptionsFromJSON as a substitute for PublicKeyCredential.parseRequestOptionsFromJSON, and get is provided as a substitute for navigator.credentials.get.
import {
get,
parseRequestOptionsFromJSON,
} from "@github/webauthn-json/browser-ponyfill";
const response = await fetch("<endpoint path that returns PublicKeyCredentialRequestOptions as JSON>");
const publicKeyCredentialRequestOptionsJSON = await response.json();
const credentialGetOptions = parseRequestOptionsFromJSON(publicKeyCredentialRequestOptionsJSON);
const publicKeyCredential = await get({ publicKey: credentialGetOptions });
const authenticationResponseJSON = publicKeyCredential.toJSON();
const authenticationResponseJSONStr = JSON.stringify(authenticationResponseJSON);
4.5. Modules
WebAuthn4J consists of the following four modules.
4.5.1. Core: webauthn4j-core.jar
Provides core features for WebAuthn attestation and assertion verification.
4.5.2. Metadata: webauthn4j-metadata.jar
Provides additional features regarding FIDO Metadata Service.
4.5.3. Core-Async: webauthn4j-core-async.jar
Provides async variant of core features for WebAuthn attestation and assertion verification.
4.5.4. Metadata-Async: webauthn4j-metadata-async.jar
Provides async variant of additional features regarding FIDO Metadata Service.
4.5.5. App Attest: webauthn4j-appattest.jar
Provides core features for Apple App Attest attestation and assertion verification.
4.6. Origin verification
In WebAuthn, “Origin verification” means the server verifies that the Origin recognized by the user agent (browser) at the time of the registration or authentication ceremony matches the Origin expected by the RP. This verification relies on the origin recorded in clientData and ensures the ceremony actually started from the intended page, preventing phishing and unintended cross‑origin invocation.
By default, WebAuthn does not allow cross‑origin usage within iframes (e.g., executing WebAuthn inside an iframe whose parent page is on a different origin). This is to uphold the same‑origin policy and mitigate threats such as clickjacking, or forcing authentication from another origin.
That said, there are legitimate use cases where you may want to allow cross‑origin execution in a constrained manner: Embedding an authentication widget in a partner site (different parent and RP origins).
In such cases, you should both declare permission to the client side via Permissions‑Policy directives and also configure server‑side verification appropriately. WebAuthn4J provides a ServerProperty builder API to declare acceptable origins for the RP page, and additionally to constrain acceptable top origins (the top‑level document/parent page). By default, cross‑origin usage is not allowed; you must explicitly opt in by configuring acceptable top origins.
4.6.1. origin verification
-
How to specify
-
origin(…), origins(…), originPredicate(…): Specify the acceptable origin(s) for the RP page.
-
-
Default behavior
-
Only requests whose clientData.origin matches the specified condition are accepted.
-
-
Example
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.build();
4.6.2. topOrigin verification
-
Purpose
-
When running WebAuthn in an iframe, verify the top‑level document’s origin (the parent page) and allow the ceremony only when it is framed by the expected parent.
-
-
How to specify
-
topOrigin(…), topOrigins(…), topOriginPredicate(…): Allow only the specified top origins
-
anyTopOrigin(): Allow any top origin (convenient, but expands the security boundary and should be used with care)
-
-
Default behavior
-
If you do not configure a topOriginPredicate, cross‑origin iframe usage is not allowed.
-
If the client does not provide topOrigin, only the normal origin verification is performed.
-
-
Example (allow a specific parent origin)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.topOrigin(new Origin("https://parent.example.com"))
.build();
-
Example (allow any parent origin — not recommended for production)
ServerProperty serverProperty = ServerProperty.builder()
.origin(new Origin("https://rp.example.com"))
.rpId("rp.example.com")
.challenge(challenge)
.anyTopOrigin()
.build();
-
Security considerations
-
anyTopOrigin significantly increases the attack surface. Prefer whitelisting specific parent origins with topOrigin/topOrigins/topOriginPredicate wherever possible.
-
-
Backward compatibility
-
Legacy coarse‑grained allow flags may remain for compatibility, but for new setups use topOriginPredicate (or anyTopOrigin where strictly necessary). The client’s crossOrigin value is a supplemental signal; the final allow/deny decision should be made by the server‑side topOrigin configuration.
-
4.7. Custom validator implementation
WebAuthn4J can add custom validator.
For registration validation, implement CustomRegistrationVerifier.
For authentication validation, implement CustomAuthenticationVerifier.
4.8. Custom converter implementation
WebAuthn4J uses Jackson library for JSON and CBOR serialization and deserialization.
If you would like to custom serialization or deserialization, register custom serializer or deserializer to the underlying Jackson ObjectMapper.
4.9. Classes
4.9.1. Data Transfer Objects
Classes under com.webauthn4j.data package are designed as immutable DTO.
4.9.2. Converter, Jackson Modules for WebAuthn
Classes under com.webauthn4j.data package are designed as being serializable and deserializable.
Some Classes under converter package needs custom serializer and deserializer.
Jackson’s module named
WebAuthnJSONModule and WebAuthnCBORModule consolidate these custom serializer and deserializer.
WebAuthn4J’s validators register these modules onto Jackson’s ObjectMapper automatically.
If you want to use WebAuthn4J’s serializer and deserializer outside of WebAuthnManager, you can register these modules onto Jackson’s ObjectMapper.
4.9.3. TrustAnchorsResolver
TrustAnchorsResolver interface is used by TrustAnchorCertPathTrustworthinessVerifier to explore root certificates in the verification of the authenticity of the attestation statements.
4.9.4. TrustAnchorsProvider
TrustAnchorsProvider is an interface that TrustAnchorsResolverImpl delegates TrustAnchor load operation to.
KeyStoreFileTrustAnchorsProvider is provided as an implementation for loading TrustAnchor from Java Key Store file.
WebAuthn$J Spring Security also provides CertFileResourcesTrustAnchorProvider to load TrustAnchor from Spring Resource.
4.10. Using FIDO CTAP2 Security key in your own application other than WebAuthn
For FIDO CTAP2 Security key, WebAuthn is just an application. An original application can use a a security key too. This section describes how to use WebAuthn4J for attestation and assertion validation in your own application using the FIDO CTAP2 security key.
4.10.1. Registration & Authentication flow of your own application using FIDO CTAP2 security key
If you use FIDO CTAP2 security key for authentication in your own application, you need to register the security key first.
Call the authenticatorMakeCredential method of the security key to retrieve the "Attestation" data, which contains public key and device configuration and save it.
The obtained attestation data need to be verified to determine if the security key is acceptable for the application.
WebAuthn4J can verify the attestation with CoreRegistrationVerifier class.
For authentication, the application need to call the authenticatorGetAssertion method of the security key to retrieve the "assertion" data, which contains signature.
By validating the retrieved assertion, the application can determine whether the security key used for authentication is the same as the one used for registration, and can determine whether the access is legitimate.
WebAuthn4J can verify the assertion with CoreAuthenticationVerifier class.
4.10.2. How to verify application specific client data
Implementing the above flow will provide authentication feature, but if the entity that calls the FIDO CTAP2 security key (client) and the entity that verifies the attestation and the assertion are separated, in some cases, an application specific client data is needed to be verified at the server at registration and authentication.
The client data itself can be sent together with the attestation and assertion, but in order to protect the client data from MITM attacks, it need to be signed and protected.
In FIDO CTAP2 specification, there is a parameter named clientDataHash that is common to authenticatorMakeCredential method used at registration and authenticatorGetAssertion method used at authentication.
Since the security key generates a signature from data that contains clientDataHash, an application can verify its specific client data by setting clientDataHash to the hash of the client data and validating the signature.
5. FAQ
5.1. What should be specified in excludeCredentials of PublicKeyCredentialCreationOptions?
The excludeCredentials parameter of PublicKeyCredentialCreationOptions is used to specify Authenticators that you want to exclude during registration.
It can be used to exclude Authenticators that are already linked to an account from new registrations.
// When there are no Authenticators to exclude
List<PublicKeyCredentialDescriptor> excludeCredentials = null;
// When excluding specific Authenticators
List<PublicKeyCredentialDescriptor> excludeCredentials = Collections.singletonList(
new PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY,
existingCredentialId,
transports
)
);
-
When null is specified, no Authenticators will be excluded
-
By specifying the existing authenticators in
excludeCredentials, you can prevent accidental re-registration of the same authenticator
5.2. What should be specified in allowCredentials of PublicKeyCredentialRequestOptions?
The allowCredentials parameter of PublicKeyCredentialRequestOptions is used to specify available Authenticators during authentication.
For non-discoverable credentials, you need to specify the credentialId in allowCredentials.
// When allowing all Authenticators
List<PublicKeyCredentialDescriptor> allowCredentials = null;
// When allowing only specific Authenticators
List<PublicKeyCredentialDescriptor> allowCredentials = Collections.singletonList(
new PublicKeyCredentialDescriptor(
PublicKeyCredentialType.PUBLIC_KEY,
credentialId,
transports
)
);
5.3. Isn’t the WebAuthnManager class responsible for too many things?
Q: The WebAuthnManager provides two different functions, registration processing and authentication processing.
Even when calling from a class that only requires authentication processing, Attestation-related settings, which are only necessary for registration processing, are needed when instantiating WebAuthnManager, which is inconvenient.
A: We provide WebAuthnRegistrationManager and WebAuthnAuthenticationManager, which are separate components of WebAuthnManager, so please use them instead.
=== WebAuthnManager class has too many responsibility
Q: WebAuthnManager provides two distinct functions: registration and authentication.
This can be inconvenient for classes that only need authentication function, as they must still configure attestation settings required only for registration when instantiating WebAuthnManager.
A: Please consider using WebAuthnRegistrationManager and WebAuthnAuthenticationManager, each dedicated to either registration or authentication.