WebAuthn: Server and Client-Side Strong Auth Library For JavaScript

François Zaninotto
François ZaninottoJune 24, 2019
#security#js#oss

Browsers support a new security standard called WebAuthn to facilitate strong authentication. We've open-sourced a library to facilitate its implementation. Read on to know more.

Strong Authentication Is Complicated

Let's talk security.

Passwords suck: they are hard to remember when they are long enough to be secure, we need to generate one new password for each new service, and about 550M passwords are forbidden because they were part of a data breach.

What are the alternative authentication methods? Instead of a knowledge factor (something the user knows), we need to rely on a possession factor (something the user has, like a security token) or inherent factors (something the user is, like fingerprints). Or, ideally, on a combination of those (that's multi-factor authentication, or MFA).

Unfortunately, not every user has a device with a fingerprint sensor, and software tokens (like the Google Authenticator app) require that the user copies a one time password (OTP) from the token to the service they want to log in to. In other words: strong authentication is cumbersome for the end used.

One good solution is a connected token, a device that plugs to the computer, like a USB key. It's a possession factor, you can use it on any computer. You don't need to copy a OTP by hand as it's connected to the computer. It uses public key cryptography to validate a challenge with a single click.

Here is such a connected token, called a YubiKey and sold by a company called Yubico:

Yubikey

But services that need to access the token must know its protocol. And until recently, only thick clients could communicate with connected tokens. That excluded services running in a browser.

WebAuthn / Fido2: A W3C standard bringing Strong Auth to the Browser

Enter FIDO2, a standard for strong authentication in the browser. To quote Wikipedia:

At its core, FIDO2 consists of the W3C Web Authentication (WebAuthn) standard and the FIDO Client to Authenticator Protocol (CTAP).

The WebAuthn standard is a set of JavaScript APIs for communicating with a connected token in a browser. At the time of writing, it's supported in most desktop browsers, covering 67% of users worldwide:

Can I Use statistics for WebAuthn

What it allows to do is to use a connected token to register and log in to a web application. No password to remember: just plug the key, press the button, and you're done. Here is a little demo:

YouTube might track you and we would rather have your consent before loading this video.

Always allow

If you've ever used a YubiKey or something similar, you'll agree that the experience is an order of magnitude better than entering a password. But then, why don't we all use WebAuthn in our web apps?

Client Server Protocol Is Hard

Well, do you remember the Client to Authentication Protocol (CTAP) in the FIDO2 project? It's a protocol, meaning that it's up to the developer to implement it, and here lies the problem.

There are a few implementations already, that you'll find on GitHub and in the webauthn.io website.

WebAuthn implementations

These implementations are usually proof-of-concepts developped by people close to the FIDO2 alliance, meaning that these libraries work in only a few conditions, and their documentation is either nonexistent or hard to understand.

For instance, here is the README of the JavaScript webauthn-demo by the FIDO alliance itself:

Documentation of JS WebAuthn code .

Good luck figuring out how to use that. And it's the only one in JavaScript.

Introducing the @webauthn Packages

We work with Wallix, a leading Privilege Access Management (PAM) vendor based in Paris. For a (secret) project we're developing for them, Wallix asked Marmelab to allow users to log into the web application using a YubiKey token. So we had to figure out how to use that WebAuthn protocol.

After much sweating, we've managed to implement both the client and server side of the FIDO2 protocol. Wallix has agreed to open-source our work, so here it is. Today, we're introducing @webauthn/client and @webauthn/server, two npm packages that will help JavaScript developers implement FIDO2 in practice. We've also included an example client and server based in these packages.

Here is how to implement a Register button on the client side:

import { solveRegistrationChallenge, solveLoginChallenge } from '@webauthn/client';

const loginButton = document.getElementById('login');
const registerButton = document.getElementById('register');
const messageDiv = document.getElementById('message');

const displayMessage = message => {
    messageDiv.innerHTML = message;
}

registerButton.onclick = async () => {
    const challenge = await fetch('https://localhost:8000/request-register', {
        method: 'POST',
        headers: {
            'content-type': 'Application/Json'
        },
        body: JSON.stringify({ id: 'uuid', email: 'test@test' })
    })
        .then(response => response.json());
    const credentials = await solveRegistrationChallenge(challenge);

    const { loggedIn } = await fetch(
        'https://localhost:8000/register',
        {
            method: 'POST',
            headers: {
                'content-type': 'Application/Json'
            },
            body: JSON.stringify(credentials)
        }
    ).then(response => response.json());

    if (loggedIn) {
        displayMessage('registration successful');
        return;
    }
    displayMessage('registration failed');
};

loginButton.onclick = async () => {
    const challenge = await fetch('https://localhost:8000/login', {
        method: 'POST',
        headers: {
            'content-type': 'Application/Json'
        },
        body: JSON.stringify({ email: 'test@test' })
    })
    .then(response => response.json());


    const credentials = await solveLoginChallenge(challenge);
    const { loggedIn } = await fetch(
        'https://localhost:8000/login-challenge',
        {
            method: 'POST',
            headers: {
                'content-type': 'Application/Json'
            },
            body: JSON.stringify(credentials)
        }
    ).then(response => response.json());

    if (loggedIn) {
        displayMessage('You are logged in');
        return;
    }
    displayMessage('Invalid credential');
};

The two methods exposed by @webauthn/client are just a thin wrapper over the Navigator.credentials API. Indeed, the browser manages the cryptographic challenge so that developers don't try to do it themselves (the wrong way).

And here is the server part, using Node.js and Express.js:

const {
    generateRegistrationChallenge,
    parseRegisterRequest,
} = require('@webauthn/server');

const app = express();
app.use(cors());
app.use(bodyParser());

app.post('/request-register', (req, res) => {
    const { id, email } = req.body;

    const challengeResponse = generateRegistrationChallenge({
        relyingParty: { name: 'ACME' },
        user: { id, name: email }
    });

    userRepository.create({
        id,
        email,
        challenge: challengeResponse.challenge,
    })

    res.send(challengeResponse);
});

app.post('/register', (req, res) => {
    const { key, challenge } = parseRegisterRequest(req.body);

    const user = userRepository.findByChallenge(challenge);

    if (!user) {
        return res.sendStatus(400);
    }

    userRepository.addKeyToUser(user, key);

    return res.send({ loggedIn: true });
});

const config = {
    cert: fs.readFileSync(path.resolve(__dirname, '../tls/localhost.pem')),
    key: fs.readFileSync(path.resolve(__dirname, '../tls/localhost-key.pem'))
};

spdy.createServer(config, app).listen(8000, () => {
    console.log('Server is listening at https://localhost:8000. Ctrl^C to stop it.');
});

The code is available on GitHub under the Apache2 license: wallix/webauthn.

Conclusion

Security is hard, emerging technologies are hard, and both are even harder when you're on your own. By sharing what we've learned and built for Wallix on the FIDO2 protocol, we hope that you'll find it easier to implement Strong Authentication or Two-Factor Authentication on your web applications.

As it's an open-source effort, feel free to contribute by opening issues and pull requests on GitHub.

Did you like this article? Share it!