Various WebAuthn/passkey-related questions have been trending in the Vaadin community, and unsurprisingly so. In 2024, "passkeys" should become the only acceptable authentication method for any self-respecting software craftsman, and passwords should be burnt with 🔥.
Some questions have been rather low-level, such as how to call the raw browser API from Vaadin web apps. To those asking, I'd rhetorically question: are you developing the right thing? For most Vaadin apps, it would be better to use WebAuthn indirectly. For instance, if you use an identity management server/SSO, like KeyCloak, you should probably configure it to enable passkeys instead. Alternatively, consider utilizing it through something like Spring Security.
Sometimes there can be reasons to build the integration yourself. Usually, the reasons are misguided, but learning and understanding the concepts is a valid one. As there has been recent interest in instructions for low-level utilization of the WebAuthn browser API, and since the use case also serves as a generic example of consuming modern Promise-based JavaScript APIs from Vaadin, I decided to construct an example from scratch to implement basic authentication and authorization with WebAuthn, with minimal additional dependencies.
Disclaimer: If you find yourself needing to implement WebAuthn from scratch in your real-world application, I strongly recommend learning and fully understanding the maths behind passkey authentication. A good starting point would be this entertaining and informative presentation from JFokus 2024, which I had the opportunity to attend live last week.
Enough with the warnings; let's examine the most relevant parts of this example project.
To manage the public-key cryptography, I've incorporated the com.yubico:webauthn-server-core
dependency from Yubico. The application uses Spring Boot primarily to launch Tomcat. For example, Spring Security is not used so that you can understand what actually happens behind the scenes. A bit of "dependency injection magic" is used for some classes, but removing this should be fairly simple if you're allergic to it.
The codebase contains a lot of comments on non-trivial parts, making it suitable as learning material. But please check out at least these parts and the sequence diagram below.
The WebAuthnSession
abstracts the WebAuthn details for the UI and maintains the session. It delegates the registration and validation requests initiated by WebAuthnService
to the browser API and returns the credentials back to it. It is naturally used in the LoginAndRegistrationView
and also by the AccessControlChecker
, which implements a straightforward approach: it does not manage roles but simply allows logged-in users access to the actual app view(s).
A part of WebAuthnSession
that might interest Vaadin developers who don't care about WebAuthn itself is how we call the asynchronous browser(JavaScript) API from the server-side Java code. For example, here:
AssertionRequest assertionRequest = webAuthnService.startReauthentication(username);
try {
String credJson = assertionRequest.toCredentialsGetJson();
JsPromise.computeString("""
// raw credential JSON (binary fields b64d)
var c = %s;
// convert binary fields from b64 to bytes
fromB64Cred(c);
const cred = await navigator.credentials.get(c);
return createCredentialJsonForServer(cred);
""".formatted(credJson)).thenAccept(credentialJson -> {
try {
webAuthnService.finishAssertion(assertionRequest, credentialJson);
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
}
We use the await
syntax in the script body. This essentially makes the rest of the script body a Promise, and the final 'resolved' value of the script is returned to the server asynchronously as a CompletableFuture
. In this case, there is a user interaction happening between (and most likely, for example, fingerprint scanning) before the finishAssertion
method is called. Currently, the await keyword needs the Viritin add-on, but a PR was recently merged to land a similar feature to the core in Vaadin 24.4.0.
With the help of CompletableFuture
and Promise
s/async
keyword on the JS side, the final API used in the UI code becomes clean and maintainable, even though it contains asynchronous logic spread over to both the client and the server. Here is what happens, e.g., when clicking the register button:
TextField username = new VTextField("Username");
Button register = new Button("Register!", e -> {
webAuthnSession.registerUser(username.getValue()).thenAccept(v -> {
Notification.show("Username %s succesfully registered. Welcome!"
.formatted(username.getValue()))
.setPosition(Notification.Position.MIDDLE);
UI.getCurrent().navigate(MainView.class);
}).exceptionally(ex -> {
Notification.show("Failed to create user. " + ex.getMessage());
return null;
});
});
The server-side configuration and com.yubico:webauthn-server-core
usage is in WebAuthnService. I built it pretty much by copy-pasting from Yubico's documentation. The Yubico's API needs a CredentialsRepository
implementation, which I quickly hacked together as an in-memory implementation. That interface may first appear overly complex, but that is due to the (mostly valid) assumption that a user account might have multiple passkeys and/or other authentication mechanisms.
In a real-world app, this part probably needs the most thinking: how to store the public keys in your existing user database. The nice part is that, as opposed to storing passwords in a database, it is much harder to disastrously fail to store passkeys in a non-secure way. Leaking out the public key part that the browser shares with your server wouldn't be an issue at all.
Below you can find a simplified Sequence Diagram describing what happens during a registration process. The parts that utilize public-key cryptography are marked with 🔐.
In the case of a login, the sequence is quite similar. The login process also begins on the server/service side with the creation of a claim. But now we don't need to specify the username, but let the user provide the passkey they want (users can have multiple accounts in this demo). In the login, we again validate the claim, and the service looks for an account (~ unique username in this app) that is associated with the given passkey.
Many Vaadin apps operate in areas that are "very business-critical," such as banks handling million-dollar transfers or in military/medical sectors where unauthorized access could be lethal. Therefore, I also wanted to include an additional example for re-authentication before executing a very critical action. Practically, we require a new fingerprint or facial recognition before letting the critical action go through. Just to make sure that nobody left their laptop lid open during a coffee break... The flow in such cases is quite similar to the login process, but this time, we specifically request the passkey to be the same one that was used earlier for login.
If you want to experience passkeys firsthand, you can try the deployed version. However, a better option would be to check out the source code, import it into your IDE, run the Application class, and explore it locally!
Check out the full Webauthn example on GitHub.