Add Two-Factor Authentication To Your Website with Google Authenticator and Twilio SMS

April 10, 2013
Written by
Joel Franusic
Contributor
Opinions expressed by Twilio contributors are their own

Twilio Bug Logo

Update 2021:

The Verify API now supports TOTPCheck out this blog post for the recommended approach to building TOTP in your application with some more recent techniques not highlighted below. Please get in touch if you have additional questions.

Why Two-Factor Authentication?

Back in the day, it used to be that enforcing a strong password policy was sufficient to prevent unauthorized access into your user’s accounts. (Ah, those were the days, when kids were polite and respected their elders). However, as the security landscape continues to evolve, it is becoming clear that a strong password policy is not enough any more. Today, an attacker can discover your user’s password in a variety of ways: they might find your user’s password on a stolen or lost computer, they might find their password on another site where your user used the same password, or your user’s computer might be infected with a virus that is capturing their keystrokes.

In an ideal world, we would all be authenticating ourselves using tamper-proof hardware that implements a public-key cryptographic system. In the meantime, a simple and effective way of improving the way your users authenticate themselves is a method known as “Two-Factor Authentication“, “Two-Factor Auth”, or just “TFA”. Two-Factor Authentication is a method where your users are required to log in with two “factors”: a password, and a code from a device that they carry on their person. That device used to be a special-purpose device, but these days that device can just as well be a mobile phone.

A great pattern that we are seeing for implementing two-factor authentication is to use the TOTP (Time-based One-time Password Algorithm) standard for the second authentication step. What is so cool about TOTP is that it is flexible enough to allow your users to generate their authentication tokens directly on their smart phones using a TOTP app like Google Authenticator or have their tokens sent to their mobile phone via SMS.

This means that you only need to implement and test one additional authentication scheme, but get the benefits of having two different ways that your users can get tokens.

The best way to see how this is done is to look at some code. Let’s do that now.

Here are the topics that I’ll be covering:

    1. An Example of Application
      I will start with a very basic Python application that implements password authentication and build from there.
    2. Understanding TOTP
      Before I show you how to add TOTP to that example application, I’ll explain how TOTP works.
    3. Adding Two-Factor Authentication
      Now that we know more about how TOTP works, I’ll show you how to add it to the example application that we started with.
    4. Adding Google Authenticator
      Here I will show you how to enable your users to authenticate via the Google Authenticator.
    5. Adding Twilio
      Here I will show you how to enable your users to authenticate using a code that is delivered to their phone via SMS.
    6. Try It Out Yourself
      Finally, I give you a working example of a site that can use both the Google Authenticator and SMS to do two-factor authentication.

An Example of An Application

Below is the code for a very basic website that only uses a username and password for authentication. We will start with this code, and then add two-factor authentication to it.

import os

import bcrypt
from twilio.rest import TwilioRestClient
from flask.ext.login import LoginManager
from flask import Flask
from flask import request
from flask import redirect
from flask import url_for
from flask import render_template
from flask.ext.login import login_user
from flask.ext.login import logout_user
from flask.ext.login import current_user
from flask.ext.login import login_required
from pymongo import Connection

from konfig import Konfig

app = Flask(__name__)
konf = Konfig()
app.secret_key = konf.secret_key

connection = Connection(konf.mongo_url)

login_manager = LoginManager()
login_manager.setup_app(app)

twilio = TwilioRestClient()

@login_manager.user_loader
def load_user(user_id):
    return User(user_id)

class User:
    def __init__(self, user_id):
        self.id = user_id.lower()
        self.db = connection.tfa.users
        self.account = self.db.find_one({'uid': self.id})

    def create(self):
        self.db.insert({'uid': self.id})
        self.account = self.db.find_one({'uid': self.id})

    def save(self):
        self.db.save(self.account)

    def password_valid(self, pwd):
        pwd_hash = self.account['password_hash']
        return bcrypt.hashpw(pwd, pwd_hash) == pwd_hash

    # The methods below are required by flask-login
    def is_authenticated(self):
        """Always return true - we don't do any account verification"""
        return True

    def is_active(self):
        return True

    def is_anonymous(self):
        return False

    def get_id(self):
        return self.id

@app.route("/", methods=['GET', 'POST'])
def main_page():
    opts = {}
    if request.method == 'GET':
        return render_template('main_page.html', opts=opts)
    user = User(request.form['username'])
    if not user.account or not user.password_valid(request.form['password']):
        opts['invalid_username_or_password'] = True
        return render_template('main_page.html', opts=opts)
    login_user(user)
    return redirect(url_for('user'))

@app.route("/sign-up", methods=['GET', 'POST'])
def sign_up():
    opts = {}
    if request.method == 'GET':
        return render_template('sign_up.html', opts=opts)
    user = User(request.form['username'])
    if user.account:
        opts['username_exists'] = True
        return render_template('sign_up.html', opts=opts)
    if request.form['password1'] != request.form['password2']:
        opts['passwords_do_not_match'] = True
        return render_template('sign_up.html', opts=opts)
    user.create()
    pwd_hash = bcrypt.hashpw(request.form['password1'], bcrypt.gensalt())
    user.account['password_hash'] = pwd_hash
    user.save()
    login_user(user)
    return redirect(url_for('user'))

@app.route("/user")
@login_required
def user():
    opts = {'user': current_user,
            'logged_in': True}
    return render_template('user.html', opts=opts)

@app.route("/logout")
def logout():
    logout_user()
    return redirect(url_for('main_page'))

if __name__ == "__main__":
    # Bind to PORT if defined, otherwise default to 5000.
    port = int(os.environ.get('PORT', 5000))
    if port == 5000:
        app.debug = True
    app.run(host='0.0.0.0', port=port)

If you don’t want to read over everything, you can just focus on the “main_page()” function, here:

@app.route("/", methods=['GET', 'POST'])
def main_page():
    opts = {}
    if request.method == 'GET':
       return render_template('main_page.html', opts=opts)
    user = User(request.form['username'])
    if not user.account or not user.password_valid(request.form['password']):
       opts['invalid_username_or_password'] = True
       return render_template('main_page.html', opts=opts)
    login_user(user)
    return redirect(url_for('user'))

If you’re new to Python or Flask, here is what’s going on in the code:

In the code below, the first line is a Python function decorator which tells Flask to register this function as the handler for GET and POST requests for ‘/’ (the main page)

@app.route("/", methods=['GET', 'POST'])
def main_page():
    opts = {}

This if statement below is true if the request was a “GET” request, meaning, no data was sent to us to process. In that case, we just send HTML to the user.

    if request.method == 'GET':
        return render_template('main_page.html', opts=opts)

If we get to this point, then we know that we were sent a “POST” request, hopefully with a username and password. We check to see if the user or password are invalid. If the user or password are not valid, then we display an error saying so. Otherwise, we log the user in.

    user = User(request.form['username'])
    if not user.account or not user.password_valid(request.form['password']):
       opts['invalid_username_or_password'] = True
       return render_template('main_page.html', opts=opts)
    login_user(user)
    return redirect(url_for('user'))

The rest of the code in this example deals with including additional libraries, setting up the application, defining the User class and handling other website functionality.

You can see the full source on GitHub here: https://github.com/jpf/Twilio-TFA/tree/no-tfa

Understanding TOTP

Before we get started with adding two-factor authentication to this example application, let’s take a quick detour and to learn more about how TOTP works.

As you’ll find on Wikipedia, TOTP “is an extension of the HMAC-based One Time Password algorithm HOTP to support a time based moving factor.”

Based on that involved explanation, you might be surprised to find that generating a one time token with TOTP is not actually very complicated. Here is how you generate a 6 digit TOTP code:

  1. Compute the 20 byte HMAC of a “shared secret” and a timecode using HMAC-SHA1.
  2. Deterministically select an offset inside of that 20 byte HMAC.
  3. Starting at the offset, select a 4 byte range from the 20 byte HMAC.
  4. Turn the 4 byte range into an integer.
  5. Divide that integer by 1,000,000 (or “10**6”, the “6” is the number of digits in the TOTP code)
  6. The remainder of that division is the 6 digit code – the TOTP code

If that isn’t making sense, try taking a look at RFC 6238 and this simple implementation of TOTP in Python below. If needed, you can cut and paste this code into a script and run it from your command line:

# Based on the pyotp: https://github.com/nathforge/pyotp
import base64
import hashlib
import hmac
import datetime
import time
import sys


class OTP(object):
    def __init__(self, secret, digits=6):
        self.secret = secret

    def int_to_bytestring(self, int, padding=8):
        """
        Turns an integer to the OATH specified
        bytestring, which is fed to the HMAC
        along with the secret
        """
        result = []
        while int != 0:
            result.append(chr(int & 0xFF))
            int = int >> 8
        return ''.join(reversed(result)).rjust(padding, '\0')

    def generate_otp(self, input):
        """
        @param [Integer] input the number used seed the HMAC
        Usually either the counter, or the computed integer
        based on the Unix timestamp
        """
        hmac_hash = hmac.new(
            base64.b32decode(self.secret, casefold=True),
            self.int_to_bytestring(input),
            hashlib.sha1,
        ).digest()

        offset = ord(hmac_hash[19]) & 0xf
        code = ((ord(hmac_hash[offset]) & 0x7f) << 24 |
                (ord(hmac_hash[offset + 1]) & 0xff) << 16 |
                (ord(hmac_hash[offset + 2]) & 0xff) << 8 | (ord(hmac_hash[offset + 3]) & 0xff)) # '6' is number of integers in the OTP return code % 10 ** 6 class TOTP(OTP): def __init__(self, *args, **kwargs): """ @option options [Integer] internval (30) the interval in seconds This defaults to 30 which is standard. """ self.interval = kwargs.pop('interval', 30) super(TOTP, self).__init__(*args, **kwargs) def timecode(self, for_time): i = time.mktime(for_time.timetuple()) return int(i / self.interval) def now(self): """ Generate the current time OTP @return [Integer] the OTP as an integer """ return self.generate_otp(self.timecode(datetime.datetime.now())) def at(self, date): """ Generate the current time OTP @return [Integer] the OTP as an integer """ return self.generate_otp(self.timecode(date)) if __name__ == "__main__": secret = "AAAAAAAAAAAAAAAA" unixtime = 0 if len(sys.argv) > 1:
        unixtime = int(sys.argv[1])
    if unixtime > 1:
        date = datetime.datetime.fromtimestamp(unixtime)
    else:
        date = datetime.datetime.now()
    totp = TOTP(secret)
    print "TOTP token for secret '%s' at '%s' is: %s" % (
        secret, date, totp.at(date))

 

Adding Two-Factor Authentication

Now that you have a better understanding of how TOTP works, let’s see how we would add it to the example web application that we started with. The first place that we’ll be touching in our example above is the “main_page()” function, here’s what it looks like when it is modified to support logging in with two-factor authentication:

@app.route("/", methods=['GET', 'POST'])
def main_page():
    opts = {}
    if request.method == 'GET':
       return render_template('main_page.html', opts=opts)
    user = User(request.form['username'])
    if not user.account or not user.password_valid(request.form['password']):
       opts['invalid_username_or_password'] = True
       return render_template('main_page.html', opts=opts)
    totp_enabled = False
    for totp_type in ['totp_enabled_via_app', 'totp_enabled_via_sms']:
       if totp_type in user.account:
           totp_enabled = user.account[totp_type]
    if totp_enabled:
       session['uid'] = user.get_id()
       session['stage'] = 'password-validated'
       return redirect(url_for('verify_tfa'))
    else:
       login_user(user)
       return redirect(url_for('user'))

This should all look very familiar, it’s all the same code as before, with some modifications at the end.

Let’s go over these modifications in a little more detail.

Here is the code that we started out with. No big changes here.

@app.route("/", methods=['GET', 'POST'])
def main_page():
    opts = {}
    if request.method == 'GET':
       return render_template('main_page.html', opts=opts)
    user = User(request.form['username'])
    if not user.account or not user.password_valid(request.form['password']):
       opts['invalid_username_or_password'] = True
       return render_template('main_page.html', opts=opts)

Here is where we add our modifications. We start by checking to see if the user has one enabled two-factor authentication on their account via an app or SMS.

    totp_enabled = False
    for totp_type in ['totp_enabled_via_app', 'totp_enabled_via_sms']:
       if totp_type in user.account:
           totp_enabled = user.account[totp_type]

If either two-factor authentication method is enabled, the “totp_enabled” variable will be True.

If TOTP is enabled, we record the fact that the user’s password has been validated into the session state, and redirect the user’s browser to “/verify-tfa”.

Otherwise, if the user doesn’t have any form of two-factor authentication enabled, then we just log them in as before.

    if totp_enabled:
       session['uid'] = user.get_id()
       session['stage'] = 'password-validated'
       return redirect(url_for('verify_tfa'))
    else:
       login_user(user)
       return redirect(url_for('user'))

In the case where the user has two-factor authentication enabled, they will be redirected to a page to enter in their two-factor authentication token. Here is what the code behind that page looks like:

@app.route("/verify-tfa", methods=['GET', 'POST'])
def verify_tfa():
    user = User(session['uid'])
    opts = {'user': user}
    if request.method == 'GET':
       opts['sms_sent'] = user.send_sms()
       return render_template('verify_tfa.html', opts=opts)
    if not session['uid']:
       opts['error-no-username'] = True
       return render_template('verify_tfa.html', opts=opts)
    if session['stage'] != 'password-validated':
       opts['error-unverified-password'] = True
       return render_template('verify_tfa.html', opts=opts)
    if user.totp.valid(request.form['token']):
       login_user(user)
       session['stage'] = 'logged-in'
       return redirect(url_for('user'))
    else:
       opts['error-invalid-token'] = True
       return render_template('verify_tfa.html', opts=opts)

And here’s what that code does. Again, this is a Python method decorator that registers this method to handle “GET” and “POST” requests to the “/verify-tfa” path.

@app.route("/verify-tfa", methods=['GET', 'POST'])
def verify_tfa():

This loads in the user information from the session state that we saved before.

    user = User(session['uid'])
    opts = {'user': user}

If the request was a “GET” request, then we send the user an SMS with their token (if they have that configured) and then render the page prompting the user to enter their token.

    if request.method == 'GET':
       opts['sms_sent'] = user.send_sms()
       return render_template('verify_tfa.html', opts=opts)

This code does some sanity checking on the session data. We should never actually get into a state where this code will run, it’s here Just In Case.

    if not session['uid']:
       opts['error-no-username'] = True
       return render_template('verify_tfa.html', opts=opts)
    if session['stage'] != 'password-validated':
       opts['error-unverified-password'] = True
       return render_template('verify_tfa.html', opts=opts)

If we got to this point, we know that we got a POST request. We check to see if the user submitted a token, and if that token is valid for the user. If the token is valid, log the user in and send them to their user page!

    if user.totp.valid(request.form['token']):
       login_user(user)
       session['stage'] = 'logged-in'
       return redirect(url_for('user'))

Otherwise, if the token wasn’t valid, ask the user to enter their token again.

    else:
       opts['error-invalid-token'] = True
       return render_template('verify_tfa.html', opts=opts)

 

Adding Google Authenticator

Now that we have code to add TOTP authentication to the login process, let’s take a look at how we’ll get our users to enable TOTP authentication on their account.

Keep in mind that you don’t have to use Google Authenticator for this to work. TOTP is an IETF standard and has many different client implementations.

I’m using Google Authenticator in this example because it seems to be the most widely used TOTP client at this time. Let’s get started.

Here is the basic overview of how this part works:

  • The user visits a page to add Google Authenticator to their account.
  • The page contains a QR code that the user scans with Google Authenticator.
  • After scanning the QR code, the user will enter the 6 digit token that Google Authenticator displays

This is what the page will look like:

And here is the code that handles takes the 6 digit token and adds it to the user’s account.

@app.route("/enable-tfa-via-app", methods=['GET', 'POST'])
@login_required
def enable_tfa_via_app():
    opts = {'user': current_user}
    if request.method == 'GET':
       return render_template('enable_tfa_via_app.html', opts=opts)
    token = request.form['token']
    if token and current_user.totp.valid(token):
       current_user.account['totp_enabled_via_app'] = True
       current_user.save()
       return render_template('enable_tfa_via_app.html', opts=opts)
    else:
       opts['token_error'] = True
       return render_template('enable_tfa_via_app.html', opts=opts)

By this point, you should be familiar with the first part of this function.

Starting at line 7, we check to see if the token that the user entered is valid, this is an important step that makes sure that the user set up Google Authenticator correctly.

Once we know that the token is valid, we update a flag on the user’s account and save the update. The details of how this data is persisted will differ from application to application, so we don’t cover those details in this tutorial.

If there was an error with the token, notify the user and ask them to try again.

Finally, I wanted to show you how I generate the QR code. I’m generating my own QR code to avoid sending the shared secret to another service and reduce the risk of leaking the shared secret in a plaintext string.

Each account in Google Authenticator has an account name, so in this section we create a globally unique name for our user and then have our totp object make us an image with the QR code that will contain that account name and the TOTP shared secret for that account.

The object that the “.qrcode()” method can only write the image to a file. So here, we create a “StringIO”, a file-like object in memory, to write to. We write the image to that object and then send the contents over the wire.

@app.route('/auth-qr-code.png')
@login_required
def auth_qr_code():
    domain = urlparse.urlparse(request.url).netloc
    if not domain:
       domain = 'example.com'
    username = "%s@%s" % (current_user.id, domain)
    qrcode = current_user.totp.qrcode(username)
    stream = StringIO.StringIO()
    qrcode.save(stream)
    image = stream.getvalue()
    return Response(image, mimetype='image/png')

As I noted earlier, I’m glossing over several things here. I’m not showing you how I persist the user objects and I’m not showing you the HTML that gets sent to the user. If you’re wondering what those look like, take a look at the full source over here: https://github.com/jpf/Twilio-TFA

Adding Twilio

Once we’ve added support for Google Authenticator, it’s just a small additional step to give our users the ability to receive TOTP tokens on their phones via SMS. The procedure for adding SMS based two-factor authentication is very similar to adding support for Google Authenticator. The main difference is that instead of having our users scan a QR code, we have them enter in their mobile phone number.

Here is what this page looks like:

And here is the code that powers the page above.

@app.route("/enable-tfa-via-sms", methods=['GET', 'POST'])
@login_required
def enable_tfa_via_sms():
    opts = {'user': current_user}
    if request.method == 'GET':
       return render_template('enable_tfa_via_sms.html', opts=opts)
    if 'phone_number' in request.form and request.form['phone_number']:
       current_user.account['phone_number'] = request.form['phone_number']
       current_user.save()
       opts['sms_sent'] = current_user.send_sms(ok_to_send=True)
       opts['phone_number_updated'] = True
       return render_template('enable_tfa_via_sms.html', opts=opts)
    token = request.form['token']
    if token and current_user.totp.valid(token):
       current_user.account['totp_enabled_via_sms'] = True
       current_user.save()
       return render_template('enable_tfa_via_sms.html', opts=opts)
    else:
       opts['token_error'] = True
       return render_template('enable_tfa_via_sms.html', opts=opts)

In this code, we check to see if we got a phone number in the “POST” request.

If so, we take the phone number and send an SMS with the TOTP code to that phone number. We store the results of the SMS sending method in an option called “sms_sent”, if the SMS sending method returns “False” then the HTML template will display an error to the user saying that the phone number was invalid.

Assuming that previous section of code worked, the user should have recieved an SMS with their TOTP code and then entered that code into the page. In this part of the code, we check to see if we were give a token and see if that token is valid. If the token is valid, we enable two-factor authentication for this account and save that setting. As I said in the section on adding Google Authenticator, the details of how to persist the user settings will be unique to your situation, so I don’t cover that here. If there was an error validating the token, then we notify the user of that error via a flag that will be checked by our HTML template.

Summary

In this article, we showed you an example of a simple web application written in Python/Flask. We showed you how to add two-factor authentication to that application, Finally, we showed you how you would enable your users to use Google Authenticator or SMS to authenticate to that modified application.

What I didn’t cover is the HTML that I used in this example or how I saved the user data.

To really understand what is going on, I suggest that you try out the example I have running online and then look at the code.

Try it out yourself

If you want to see what this all looks like, here is a copy of the code running online for you to try out: http://twilio-tfa.herokuapp.com

All of the source code for this example is also available on GitHub: https://github.com/jpf/Twilio-TFA

Thanks for reading.

Please let me know if you have any additional questions, feedback or patches.