Let us have a look at the FIDO2 authentication sequence diagram:
The blue part is handled by the WebAuthn client (e.g. the web browser) and the green parts are handled by the WebAuthn server library.
A server-side WebAuthn library has 4 basic functions: Start registration, Finish registration, Start authentication and Finish authentication. Below is an example of how these functions can be used in a web server:
// handles HTTPS requests to /register
public Either<String, RegistrationRequest> startRegistration(String username, String displayName, String credentialNickname) {
...
if (userStorage.getRegistrationsByUsername(username).isEmpty()) {
byte[] userId = challengeGenerator.generateChallenge();
RegistrationRequest request = new RegistrationRequest(
username,
credentialNickname,
U2fB64Encoding.encode(challengeGenerator.generateChallenge()),
rp.startRegistration(
new UserIdentity(username, displayName, userId, Optional.empty()),
Optional.of(userStorage.getCredentialIdsForUsername(username)),
Optional.empty()
)
);
registerRequestStorage.put(request.getRequestId(), request);
return new Right(request);
} else {
return new Left("The username \"" + username + "\" is already registered.");
}
}
// handles HTTPS requests to /register/finish
public Either<List<String>, SuccessfulRegistrationResult> finishRegistration(String responseJson) {
...
RegistrationResponse response = null;
...
response = jsonMapper.readValue(responseJson, RegistrationResponse.class);
...
RegistrationRequest request = registerRequestStorage.getIfPresent(response.getRequestId());
registerRequestStorage.invalidate(response.getRequestId());
...
Try<RegistrationResult> registrationTry = rp.finishRegistration(
request.getPublicKeyCredentialCreationOptions(),
response.getCredential(),
Optional.empty()
);
if (registrationTry.isSuccess()) {
return Right.apply(
new SuccessfulRegistrationResult(
request,
response,
addRegistration(
request.getUsername(),
request.getPublicKeyCredentialCreationOptions().user(),
request.getCredentialNickname(),
response,
registrationTry.get()
),
registrationTry.get().attestationTrusted()
)
);
} else {
...
return Left.apply(Arrays.asList("Registration failed!", registrationTry.failed().get().getMessage()));
}
...
}
// handles HTTPS requests to /authenticate
public AssertionRequest startAuthentication(Optional<String> username) {
...
AssertionRequest request = new AssertionRequest(
username,
U2fB64Encoding.encode(challengeGenerator.generateChallenge()),
rp.startAssertion(
username.map(userStorage::getCredentialIdsForUsername),
Optional.empty()
)
);
assertRequestStorage.put(request.getRequestId(), request);
return request;
}
// handles HTTPS requests to /authenticate/finish
public Either<List<String>, SuccessfulAuthenticationResult> finishAuthentication(String responseJson) {
...
final AssertionResponse response;
...
response = jsonMapper.readValue(responseJson, AssertionResponse.class);
...
AssertionRequest request = assertRequestStorage.getIfPresent(response.getRequestId());
assertRequestStorage.invalidate(response.getRequestId());
...
Optional<String> returnedUserHandle = Optional.ofNullable(response.getCredential().response().userHandleBase64());
final String username;
if (request.getUsername().isPresent()) {
username = request.getUsername().get();
} else {
username = userStorage.getUsername(returnedUserHandle.get()).orElse(null);
}
final String userHandle = returnedUserHandle.orElseGet(() ->
username == null
? null
: userStorage.getUserHandle(username)
.map(BinaryUtil::toBase64)
.orElse(null)
);
...
Try<AssertionResult> assertionTry = rp.finishAssertion(
request.getPublicKeyCredentialRequestOptions(),
response.getCredential(),
() -> userHandle,
Optional.empty()
);
if (assertionTry.isSuccess()) {
final AssertionResult result = assertionTry.get();
if (result.success()) {
...
userStorage.updateSignatureCountForUsername(
username,
response.getCredential().id(),
result.signatureCount()
);
...
return Right.apply(
new SuccessfulAuthenticationResult(
request,
response,
userStorage.getRegistrationsByUsername(username)
)
);
...
}
} else {
...
return Left.apply(Arrays.asList("Assertion failed!", assertionTry.failed().get().getMessage()));
}
...
}
In the example above assertRequestStorage
and registerRequestStorage
are a DAO that stores
challenges temporarily. The other DAO, userStorage
, persists data permanently.
Note
|
WebAuthn only works on HTTPS webpages. |
This section assumes that you are building a web site. If this is not the case, have a look at our FIDO2 host libraries instead.
The main part of the client is to be a middle-man between the server and the FIDO2 compliant device.
The straightforward way to use FIDO2 in a supported browser is to use the Web Authentication API, which exposes two functions:
navigator.credentials.create
|
Register using a FIDO2 device. |
navigator.credentials.get
|
Authenticate using a FIDO2 device. |
<script>
if (!window.PublicKeyCredential) { /* Platform not capable. Handle error. */ }
var publicKey = {
challenge: {challenge}, // The challenge must be produced by the server
// Relying Party:
rp: {
name: "Demo server"
},
// User:
user: {
id: {user_id}, // id may be generated by the server
name: "a.user@example.com",
displayName: "A User",
icon: "https://example.com/image.png"
},
// This Relying Party will accept either an ES256 or RS256 credential, but
// prefers an ES256 credential.
pubKeyCredParams: [
{
type: "public-key",
alg: -7 // "ES256" as registered in the IANA COSE Algorithms registry
},
{
type: "public-key",
alg: -257 // Value registered by this specification for "RS256"
}
],
excludeCredentials: [],
attestation: 'direct',
timeout: 60000,
extensions: {"loc": true} // Include location information in attestation
};
// Note: The following call will cause the authenticator to display UI.
navigator.credentials.create({ publicKey })
.then(function (attestation) {
// Send new credential info to server for verification and registration.
}).catch(function (err) {
// No acceptable authenticator or user refused consent. Handle appropriately.
});
</script>
<script>
if (!window.PublicKeyCredential) { /* Platform not capable. Handle error. */ }
navigator.credentials.get({
publicKey: {
rpId: document.domain,
challenge: {challenge}, // The challenge must be produced by the server
allowCredentials: [
{
type: 'public-key',
id: {credential_id} // The credential_id may be provided by the server
}
],
timeout: 60000
}
}).then(function (assertion) {
// Send new credential info to server for verification and registration.
}).catch(function (err) {
// No acceptable authenticator or user refused consent. Handle appropriately.
});
</script>
For a complete example, see this demo server.
For complete example code (both server and client) in various languages, have a look at respective FIDO2 library's accompanied demo server.