Adding second factor authentication with FIDO U2F
This is the first semester since the Fall of 2015 that I have not taught a course with the Foundation for Advanced Education in the Sciences. It was a pleasure teaching and I was lucky enough to spend most of my time on a course I had designed. For the Spring 2016 semester I designed the syllabus and began teaching a course on machine learning and object oriented python. I chose to include a web application as I felt it exposed the students to some unfamiliar ideas.
Most of the students were fellow scientists. Many only had previous experience writing scripts for use in their own research. Not trusting user input was often a novel concept. During the course I only had a couple of hours to introduce web applications. This meant I skipped over many important topics. I intend this post to be the first in a collection moving beyond the basics for anyone still new to these concepts. I will start with a basic background but the actual implementation will hopefully be new for most. If implementing web application authentication is familiar to you then skip ahead to the implementation.
Let me know if there are topics you think I should cover moving forward.
In this post I will cover authentication, specifically adding a second authentication factor for additional security.
21st January 2023: An update to this post is now available migrating to WebAuthn.
Authentication
When a program we write interacts with the world beyond our direct control we have to be prepared for inputs we did not expect. Many of these will be unintentional or benign and regular error checking is appropriate and sufficient. This post discusses attempts to deliberately gain access to our program and steal data or damage the program or the equipment it controls. Granting a user enhanced privileges can only be safely done if we can first verify the users identity.
Authentication is the process of verifying the truthfulness of a fact claimed true by another entity. In the context of web applications this is the identity claimed by a website visitor.
The most common approach to authentication is requesting a username and password. The username is the identity being claimed and the password is an authentication factor, specifically a knowledge factor. The expectation is that only the user will know their password.
Two factor authentication
Unfortunately this expectation often fails and hacked accounts are a common occurrence. There are multiple approaches to strengthening this process. Many can be used simultaneously. In this post I will focus on two-factor authentication and possibly cover other options in future posts.
There are two other types of authentication factor besides knowledge factors. An ownership factor is something the user has, such as an ID card or phone. An inherence factor is something the user is or does, such as a signature. Two-factor authentication combines two different types of authentication factor. For web applications these two factors are usually a knowledge factor and an ownership factor. The password is still used but it is supplemented by verifying the visitor has access to something we know the user owns.
You may have been asked by a website to input a code sent to your phone as an sms message, displayed in an app or on a hardware security token after submitting your username and password. A code via sms is better than only a password but is the weakest of the approaches taken for two-factor authentication. Inputting a code from a smartphone app or hardware token has already been covered by Miguel Grinberg for flask applications.
In the three years since Miguel wrote his post a new protocol has been gaining traction called U2F from the FIDO Alliance. It is commonly implemented as a USB dongle you simply touch when you want to authenticate. This is a convenient interface for the user but is not easily added to the application Miguel created.
The remainder of this post will focus on adding a FIDO U2F second factor authentication step to a flask application. All the code for this second factor authentication application is hosted on github.
Requirements
- Install docker, docker-compose and docker-machine if your OS requires it. Docker enables very cleanly and easily setting up a production-like environment. Everything should work on any operating system and when we are finished a single command shuts everything down.
- Get a Yubico FIDO U2F token - this will be our second authentication factor. They can be bought at the yubico store, on amazon, etc and beyond this tutorial can be used to add an extra layer of security to many popular websites. Buying a second token provides an important backup if your primary token is ever lost or damaged.
Docker containers
We will use gunicorn to serve our application, postgresql for our database and nginx as a reverse proxy. We will use Dockerfiles to customize the application and nginx containers and a docker-compose.yml file to organize the interactions between the containers.
Our root folder will have the docker-compose.yml file and a folder for nginx and another for the application. Postgresql will be used without modification and does not need a folder.
Nginx
The nginx docker image is customized to recognize the domain u2f-demo.local and redirect all traffic to a TLS/SSL encrypted connection - https://u2f-demo.local A self-signed certificate is also created. Your browser will warn you about the certificate but it is not a security threat in this instance. U2F only works with TLS/SSL encrypted websites.
Hosts file
You will need to modify your hosts file to use the domain name rather than an IP address. Instructions for all major operating systems can be found here from Rackspace. If you are using docker-machine you can find the IP address to use by running docker-machine ip
. If you are using an operating system that can run docker directly the IP address will be 127.0.0.1
Web application
Fortunately all of the difficult processing is handled by the u2flib_server package. To add two-factor authentication to an application using FIDO U2F we need to call four functions on the backend and add two javascript functions to forms on the frontend. Our application will have seven views, only three of which are additions for handling the second factor:
- index
- Starting point with links to other views
- register
- Create a new account
- login
- Now the first step of the login process
- logout
- Log out of a session
- select_2fa
- User is redirected here after supplying correct username/password to select which second factor token they want to use
- validate_2fa
- User is sent here after choosing which token they want to use. A challenge is created on the backend using the begin_authentication function from u2flib_server. With the token in the USB connection it should begin blinking. After the user presses the button on the token a response is sent back to the server. The response is then verified by the complete_authentication function from u2flib_server.
- add_2fa
- A challenge is created using the begin_registration function from u2flib_server and sent to the browser. With the token in the USB connection it should begin blinking. The token can be given a name and after the user presses the button on the token a response is sent back to the server. The response is then verified by the complete_registration function from u2flib_server.
The code for the add_2fa view is below.
@app.route('/add-2fa', methods=['GET', 'POST'])
def add_2fa():
form = forms.AddTokenForm()
if form.validate_on_submit():
# Complete 2FA registration
registered_device = complete_registration(session['u2f_enroll'],
form.response.data)[0].json
user = models.User.query.filter_by(id=session['user']).first()
u2f_cred = models.U2FCredentials(name = form.name.data,
owner = user.id,
device = registered_device)
db.session.add(u2f_cred)
db.session.commit()
flash("Authentication token added", "success")
return redirect(url_for('login'))
# Start 2FA registration
enroll = begin_registration(app_id, [])
session['u2f_enroll'] = enroll.json
challenge = enroll.data_for_client['registerRequests'][0]['challenge']
return render_template('add_2fa.html',
challenge = challenge,
appId = app_id,
version = "U2F_V2",
form=form)
The templates use the u2f-api.js file to add the token interaction. Validation is achieved using the sign function and adding a new token is done using the register function.
The code for the add_2fa template is below.
{% extends "layout.html" %}
{% from "macros.html" import form_field_with_errors with context %}
{% block head %}
{{ super() }}
<script>
setTimeout(function() {
var appId = "{{ appId }}"
var registerRequests = [{version: "{{ version }}",
challenge: "{{ challenge }}" }];
u2f.register(appId, registerRequests, [], function(data) {
$('#response').val(JSON.stringify(data));
$('#add-2fa-form').submit();
});
}, 1000);
</script>
{% endblock %}
{% block body %}
<form method="POST" action="" id="add-2fa-form" class="account-form">
<h1>Add device</h1>
{{ form.hidden_tag() }}
{{ form_field_with_errors(form.name, placeholder="Device name") }}
<button type="submit">Submit</button>
</form>
{% endblock %}
Take a look at the complete application on github and post any questions below.