Security is a big deal these days, and as we work to secure our applications, it’s important to keep abreast of latest technologies that keep your client’s data secure. WebAuthn is one such technology. Put simply, it’s public-key authentication for the web.

Required by Google for all its employees, WebAuthn is arguably the most secure way to login your users, but it requires a physical device called a security key and comes with a whole new set of vocabulary and protocols. This article will give you a primer on WebAuthn and will demonstrate how to implement it in your Angular application. Let’s dive in.

What’s in a name

“WebAuthn” is shortened from Web Authentication, and pronounced the same way, in its unshortened form. It’s an extension of the Credentials API, found in the navigator object: window.navigator.credentials. The Credentials API includes an entire suite of authentication APIs for javascript which includes using federated identities, credential storage and more.

Spec work for WebAuthn was started in 2015 and was championed by Mike West at Google. It was developed by consortium of security experts called the FIDO Alliance, created by Google and Yubico. FIDO stands for “Fast ID Online”.

Why are passwords and 2FA not good enough?

Passwords have a long, messy history of being hacked, phished, leaked, re-used and exploited. A fascinating episode of Darknet Diaries details one of the biggest password hacks in history, when a hacker obtained millions of passwords, ranked the passwords by how often they repeated, and posted them online. This enabled an untold number of additional hacks, with hackers using the most common passwords to gain access to user accounts across the web.

Two-factor authentication (2FA) adds an additional layer of protection for the user but is still vulnerable to hacks. One episode of the podcast Reply-All called The Snapchat Thief tells the story of how a user’s Instagram username was stolen using a technique called sim-swapping. 2FA is also vulnerable to phishing and man-in-the-middle attacks.

Okay, I’m sold. Let’s build it!

Supporting WebAuthn requires not only special client code, but also special server code. The node server we’ll build handles /webauthn routes via the webauthn node package, which is nice packaging of Yuriy Ackermann’s fantastic webauthn demo repo. Yuriy’s repo includes a great tutorial for building the backend yourself in node, which I suggest you try if you’d like to understand what’s happening better. It’s a complex process that I won’t cover here, but you can also read up on it in this article that Yuriy wrote. The node webauthn package also includes a client, but it is not used here.

Requirements

You’ll need one of the following to login with WebAuthn: Touch ID on your Macbook, a YubiKey, or a Google Titan security key. If you don’t have any of those, you can use the Virtual Authenticator Chrome extension, which will at least work for local development. Browser support for WebAuthn is pretty good, but be mindful of your users’ browser preference before you implement WebAuthn.

Check out the live demo of what we’re building, and accompanying repo.

Here is an overview of the Register flow:

  1. The client posts user name and email, and whatever other user data the server requires.
  2. The server response includes info about the relying party or the entity whose website you are signing up on. It also includes the attestation type, public key generation parameters and a challenge.
  3. Now our client code calls navigator.credentials.create() passing the server response, which will prompt the user with a test of user presence or TUP. In other words, prompt them to place their finger on their security key or scan their finger via Touch ID. At this point a signature specific to the user, server and device is generated and stored on the security device.
  4. The signature which includes the attestation object, or public key, is posted back to the server.
  5. The server verifies that the key matches the challenge, stores the result and sends a 200 response if all goes well.

The Login flow is very similar:

  1. The client posts the email to the server.
  2. The server retrieves the assertion challenge for that user and sends it back.
  3. Now the client calls navigator.credentials.get(), which will perform a TUP and retrieve the signature on the security device.
  4. The signature is posted back to the server.
  5. The server verifies the signature, that it is associated with the user and sends a 200 if all goes well.

Server setup

We are using a standard express server. The code below configures the webauthn package which handles the /login and /register routes. We are also handling an /auth-check route for use with your guard. It’s quite simple and the entire auth server can be setup in one file.

// webauthn routes
const webauthn = new WebAuthn({
  origin: process.env.ORIGIN || 'https://example.net',
  usernameField: 'email', // field that uniquely id's user
  userFields: {
    email: 'email',
    name: 'displayName',
  },
  store: {
    // methods to wire up your DB. (Not implemented)
    put: async (id, value) => void,
    get: async (id) => User,
    search: async (search) =>  { [username]: User },
    delete: async (id) => boolean,
  },
  rpName: 'BrieBug Software', // relying party
});

app.use('/webauthn', webauthn.initialize());

// check if the user is signed in
app.get('/auth-check', webauthn.authenticate(), (req, res) => {
  res.status(200).json({ status: 'ok'});
});

The client service

The WebAuthn client service handles all interactions with the auth server and relies on some private methods to format the responses. The login and register methods encompass the core functionality:

registerUser(user: User): Observable,[object Object]

Our service also includes an authCheck() method which you can use in a guard, a logout() method and an isSupported() method to determine if the client supports WebAuthn.

Calling the service

Now you just need to implement a login and register form. You can simply check if the form is valid and call the service when the form is submitted. In the examples below we are using reactive form groups.

Register

this.webauthnService.registerUser(this.registerGroup.value).subscribe(response => {
  if (response.status === 'ok') {
    this.router.navigate(['/']); // navigate to protected route
  }
}, error => this.error = error);

Login

this.webauthnService.loginUser(this.loginGroup.value).subscribe(response => {
  if (response.status === 'ok') {
    this.router.navigate(['/']); // navigate to protected route
  }
}, error => this.error = error);

That wasn’t so bad. The service is doing all the heavy lifting for us. Be sure you’ve included a way to display errors in your form.

Wrap up

WebAuthn adds undeniably strong security but it comes at a cost. Be sure you’re aware of these risks before you decide to adopt it: If a user loses their security key, they have lost access to their account forever, unless your server is setup to support more than one security device or WebAuthn is used as a second factor. Another consideration is that mobile support will vary, depending on how well your users keep their mobile OS up-to-date and if the phone or tablet can be used with a security key. Bluetooth security keys work better with mobile devices but are far more expensive.

You should now have a better understanding of WebAuthn and what it takes to implement it in your Angular application. WebAuthn provides top-notch security but certainly requires some investment to support it; both in development costs and hardware costs. If you would like to read more about WebAuthn, here are several resources I enjoyed: