We are going to walk through Yubico’s Java WebAuthn Server library demo registration and authentication ceremonies, explaining how the WebAuthn interaction works, step by step.
Tip
|
This walk through is designed for people who prefer to learn by doing. If you prefer learning concepts from the ground up, check out our WebAuthn Developer Guide. This walk-through and the guide are complementary to each other. |
The walk-through is divided into the following sections:
Setup: you will be setting up a local development environment on your computer to provide the starting point for following the walk-through
Overview runs through the fundamentals of WebAuthn
Registration explains the steps to register an authenticator
Authentication explains the steps to sign in to the server.
If you are new to WebAuthn, we recommend you check out https://demo.yubico.com/webauthn-technical/registration.
We are assuming that you have some familiarity with HTML, JavaScript, and Java. However, you should be able to follow along even if you are coming from a different programming language. We also assume that you are familiar with programming concepts such as functions, objects, arrays, and classes.
In addition to setting up a local development environment, you also need a YubiKey (or other authenticator).
The following software must be installed to continue with the walk-through:
Verify that you can run the webauthn demo server:
git clone https://github.com/Yubico/java-webauthn-server cd java-webauthn-server ./gradlew run
Open https://localhost:8443 to access the WebAuthn demo website.
Note
|
You will get warnings in your browser about the connection not being secure. This is expected, because this server uses a self-signed certificate. You can safely proceed to the site. |
If you get stuck, you can check Stack Overflow. If you don’t receive an answer, or remain stuck, please file an issue or open a support ticket with Yubico and we will help you out.
With WebAuthn (also known as FIDO2), public-key cryptography is used to authenticate end-users to an online service, also known as a Relying Party (RP). When the end-user registers for an online service, an RP-specific credential key pair - i.e., a private key and a public key - is generated on the authenticator and the public key is sent to the RP (the private key never leaves the authenticator). When the user makes the request to log in, the authenticator sends an assertion that proves the user possesses the private key. The RP uses the public key to validate the assertion before allowing the user to log in.
The WebAuthn demo web app is composed of four layers:
The front end layer, a.k.a. the client, is made up of the index.html
along with some JavaScript libraries such as:
js/webauthn.js
which calls the WebAuthn API methods, and
js/base64url.js
which handles the ByteArray to Base64 conversions.
The front end interacts with the server via a REST API layer that is implemented in the WebAuthnRestResource
class.
The REST API then delegates to the server layer that is implemented in the WebAuthnServer
class. This layer is where the business logic lives. The demo server implements "persistent" storage of users and credential registrations using the InMemoryRegistrationStorage
class, which implements the CredentialRepository
interface.
The server layer calls the webauthn-server-core
library layer, which implements the WebAuthn API Relying Party Operations.
View the demo web app by going to https://localhost:8443. It is a single-page app that demonstrates the WebAuthn registration and authentication ceremonies. In production systems, the WebAuthn authenticator registration functionality is typically located under the security section of the user’s profile page. For simplicity, this demo app combines both registration and authentication on a single page.
The first task of the server is to register and store the user’s WebAuthn credentials in a credential repository. The CredentialRepository
interface describes how the server looks up credentials by username or user handle. This demo stores usernames and credentials in memory. For the reference implementation, see the InMemoryRegistrationStorage
class.
As the web app starts up, it instantiates a RelyingParty
object from the webauthn-server-core
library. Your credential repository is passed in as the argument to the .credentialRepository()
method. For reference, see the WebAuthnServer
class constructor.
public class WebAuthnServer { private final RegistrationStorage userStorage; // The InMemoryRegistrationStorage implements the RegistrationStorage and CredentialRepository interfaces private final RelyingParty rp; public WebAuthnServer() { this(new InMemoryRegistrationStorage(), Config.getRpIdentity(), ...); } public WebAuthnServer(RegistrationStorage userStorage, RelyingPartyIdentity rpIdentity, ...) { this.userStorage = userStorage; ... rp = RelyingParty.builder() .identity(rpIdentity) .credentialRepository(this.userStorage) ... .build(); } ... }
Now we can initiate a request to register an authenticator via the web app at https://localhost:8443.
Enter a username
Click the Register new account button.
The JavaScript makes a call to the /register
endpoint of the REST API to initiate a registration request and passes in the username.
The server calls the rp.startRegistration()
operation, which creates a PublicKeyCredentialCreationOptions
JSON object and sets the values based on the service’s policy and preferences. In the following example you can see that the JavaScript app passed in the username "test". The server set the RP ID (rpID
) to "localhost". The rpID is important because the client - the browser in this case - protects against spoofing attacks by validating it against the origin’s effective domain. To also protect against replay attacks, the server generates a pseudo-random challenge.
{ "rp": { "name": "Yubico WebAuthn demo", "id": "localhost" }, "user": { "name": "test", "displayName": "test", "id": "eShrgFw-m1yWL_VJYKuBqOk2Wcxnkfi1v4adq7Xqr_s" }, "challenge": "g9xJT91T0xXBdsyqDXX9-tfZJBJ1rO6E8Mfiv30VCdg", "pubKeyCredParams": [ { "alg": -7, "type": "public-key" }, { "alg": -8, "type": "public-key" }, { "alg": -257, "type": "public-key" }], "excludeCredentials": [], "authenticatorSelection": { "requireResidentKey": false, "userVerification": "preferred" }, "attestation": "direct", "extensions": {} }
This registration response is returned to the client. To learn more about this data structure, see PublicKeyCredential Interface
and CredentialRequestOptions
.
The JavaScript app calls the method navigator.credentials.create()
and passes the PublicKeyCredentialCreationOptions
from the /register
response. To learn more, see Create a new credential.
At this point the client will prompt the end-user to interact with an authenticator. This experience varies based on browser or operating system. The user will be asked to use a USB security key or a platform built-in sensor. Then the user may be prompted to touch the security key, enter a PIN, and touch the security key again.
The authenticator then generates an RP-specific key-pair. It includes the public key in the AuthenticatorAttestationResponse
that is returned from the navigator.credentials.create()
method.
The AuthenticatorAttestationResponse
has an attestation object with an attestation statement that contains a signature by the private key over the attested credential public key and challenge.
The JavaScript now calls the /register/finish
endpoint of the REST API and passes along the AuthenticatorAttestationResponse
.
Once the server receives the request to finish the registration, it calls the rp.finishRegistration()
method with the AuthenticatorAttestationResponse
data. The webauthn-server-core
parses the authenticator response and verifies that the rpID
and challenge are the values it expected. It also verifies the public key and signature. If these are all correct, the server stores the credential ID, credential public key, and signature counter to the database. We recommend storing the raw attestationObject
as well for future reference.
To learn more, check out the WebAuthn Client Registration chapter of the WebAuthn Developer Guide.
Now that we have registered our credential, let us authenticate with it!
Go to https://localhost:8443 and click the Authenticate
button. The JavaScript app makes a call to the /authenticate
endpoint of the REST API and passes along the username.
The server calls the rp.startAuthentication()
operation, which creates a PublicKeyCredentialRequestOptions
JSON object and sets the values based on the service’s policy and preferences. Just as in the registration step, the server sets the rpID
and challenge. The allowCredentials
list is populated with those previously registered credentials that the user is allowed to authenticate with.
{ "challenge": "kVDORSw87Z4PwuiCKOmQ7lduC-SReKF_TLayhPLBW5c", "rpId": "localhost", "allowCredentials": [ { "type": "public-key", "id": "a_TJPMGXaqyff0ZuEVD3k3bnfiiK049rPnmWSfnNkIFW1vWYaKSgIJpIiuyUChF0Br7MDUxpbKRKVWtGKQv1tA" } ], "userVerification": "preferred", "extensions": { "appid": "https://localhost:8443" } }
This authentication response is returned to the JavaScript app.
The JavaScript app calls navigator.credentials.get()
and passes the PublicKeyCredentialRequestOptions
into the method.
At this point the client prompts the user to interact with an authenticator. This experience varies based on browser or operating system. A user is asked to use a USB security key or a platform built-in sensor. The user may be prompted to touch the security key, enter a PIN, and touch the security key again.
The authenticator matches a credential from the allowCredentials
list (recall that credentials are scoped to an rpID
), uses the associated private key to sign over the authenticator data, and returns an AuthenticatorAssertionResponse
to the JavaScript app.
The AuthenticatorAssertionResponse
contains authenticator data (rpID
and challenge) and the signature by the private key over the authenticator data.
The JavaScript app now calls the /authenticate/finish
endpoint of the REST API and passes along the AuthenticatorAssertionResponse
.
Once the server receives the request to finish the authentication, it calls the rp.finishAuthentication()
method with the AuthenticatorAssertionResponse
data. The webauthn-server-core
parses the authenticator response and verifies that the rpID
and challenge are the values it expected. It also verifies the public key and signature. If these are all correct, the server authenticates the user.
To learn more, check out the “Authentication Flow” section of the Client Authentication chapter of the WebAuthn Developer Guide.
Congratulations! You have completed all the steps to register and authenticate with a WebAuthn credential.
If you have more time, we recommend you check out Yubico’s best practices in the integration review standard and review the WebAuthn/FIDO2 Readiness Checklist.