U2F & YubiKey: Two Factor Authentication with U2F

Today I want to explain how we implemented our Two Factor Authentication using Security Keys supporting different browsers.

At Sandstorm our internal tools are mostly web–based and secured by our Single Sign On. In order to login you need to first enter your user and password and then provide a second factor for authentication. So far everyone had to insert a 6 digit PIN which changes every 30 seconds that has been provided by the Google Authenticator.

Since last week we use a hardware Security Key from Yubico as a second factor: connect it via USB, tap it and you are in.

Other than the Google Authenticator App the YubiKey exists only once in the world and you cannot duplicate it. It also implements counter–measures against Man–in–the–Middle and Phishing attacks.

Client–Side Implementation

When you log in with a valid user and password you see another login page to validate the second factor. To continue you must plugin your YubiKey and tap it. This triggers the generation of the U2F response for the U2F request sent by the server.

Which browsers support the window.u2f functions and how you can enabled them I will explain further down below.

<form id="2faForm" action="/login-u2f" method="POST"> <input type="hidden" name="u2fAuthResponse" id="u2fAuthResponse" /> <h2>Tap your YubiKey!</h2> <script> var req = JSON.parse('{{{u2fAuthRequest}}}'); if (window.u2f && window.u2f.sign) { u2f.sign(req.appId, req.challenge, [req], function(res) { document.getElementById('u2fAuthResponse').value = JSON.stringify(res); document.getElementById('2faForm').submit(); }); } else { document.write('<p><b>U2F is not supported by your browser!</b></p>'); } </script> </form>

Server–Side Implementation

Our Single Sign On server runs on Node.js and using the U2F package.  Given that the user's security key is already known, he must receive a U2F request (the challenge) and send a fitting response.

const u2f = require('u2f'); … /** * write U2F request into login page */ router.get('/login-u2f', (req, res) => const u2fAuthRequest = JSON.stringify( u2f.request(configuration.appId, req.user.u2f.keyHandle) ); req.session.u2fAuthRequest = u2fAuthRequest; res.render('see template earlier in this post', { u2fAuthRequest: u2fAuthRequest }); ); /** * validate U2F response */ router.post('/login-u2f', (req, res) => { const loginSuccess = u2f.checkSignature( JSON.parse(req.session.u2fAuthRequest), JSON.parse(req.body.u2fAuthResponse), req.user.u2f.publicKey ); … });

Before the user can login like that he has to register his Security Key device. The overall procedure is very similar to the login. The server generates a request for the user and his YubiKey provides the response.

const u2f = require('u2f'); … /** * send registration request */ router.get('/registration-u2f', ensureFullyAuthenticated, (req, res) => { const u2fRegistrationRequest = JSON.stringify(u2f.request(configuration.appId)); req.session.u2fRegistrationRequest = u2fRegistrationRequest; res.render('registration-u2f', { u2fRegistrationRequest: u2fRegistrationRequest }); }); /** * receive the registration response */ router.post('/registration-u2f', ensureFullyAuthenticated, (req, res) => { const registration = u2f.checkRegistration( JSON.parse(req.session.u2fRegistrationRequest), JSON.parse(req.body.registerResponse) ); if (registration.successful) { // store registration } else { // respond with error } });

The U2F registration HTML form is tiny. Though it is not in the example, you should make the user to re–enter his password as well.

<form action="/registration-u2f" method="POST"> <input type="hidden" name="registerResponse" id="registerResponse" /> <div id="u2fNote">Please touch the button on your U2F token for pairing!</div> <button type="submit" disabled="disabled" id="u2fSubmitButton">Submit</button> </form> <script> var req = JSON.parse('{{{u2fRegistrationRequest}}}'); if (window.u2f && window.u2f.register) { u2f.register(req.appId, [req], [], function(res) { document.getElementById('registerResponse').value = JSON.stringify(res); document.getElementById('u2fSubmitButton').disabled = false; document.getElementById('u2fNote').innerHTML = 'U2F Device Paired!'; }); } else { document.write('<p><b>U2F is not supported by your browser!</b></p>'); } </script>

Also be sure to check out the nice code examples of the U2F package itself.

Support for Google Chrome

By default up–to–date versions of Google Chrome supports U2F if the web–page enables it. In order to do so, you have to include a JavaScript API from google/u2f-ref-code, the u2f-api.js. This script exposes a Chrome package app extension which comes with the default installation. Clients do not need to install anything. 

&lt;!-- get it from https://github.com/google/u2f-ref-code/blob/master/u2f-gae-demo/war/js/u2f-api.js --&gt; &lt;script src="u2f-api.js"&gt;&lt;/script&gt;

Support for Firefox

To use U2F in Firefox you need the help of your brave users. They have to enter about:config in the address bar and accept a warning about dragons and risk. Only then they can enable the security.webauth.u2f option.

Here be dragons warning

I hope you enjoyed the post. For further reading I recommend

Thanks for reading.