2 Factor Authentication in Django

Introduction

2 Factor Authentication — or 2FA in short — is a special version of the general Multi Factor Authentication (MFA) and generally describes the idea that is also followed by Universal 2 Factor Authentication (U2F). It generally consists of something a user possesses and something a user knows.

Disclaimer

I’m not a cryptographer. I’m somebody interested in security and cryptography.

History

Authentication is something we are confronted with every single day. At least in the modern world. For most of us it probably starts the moment we look at our phone in the morning and need to unlock it to get access to the feed reader.

But that’s today, what’s was there before? Maybe decades ago? Maybe centuries ago?

Around 300 BC, the Romans passed so called “watchwords” from to 10th maniple to 9th to the 8th and eventually to the 1st maniple in front of witnesses. The 1st maniple then passed it back to the tribune who in return knew which group of maniples didn’t passed the watchword. The watchwords were used to authenticate people during the night. [1]

Passwords evolved and In the opening days of the Battle of Normandy in 1944, paratroopers of the US used a password — flash — which was presented as a challenge, and answered with the correct response — thunder.

But history shows that not only passwords have been used to authenticate people or messages. The ancient Greek and Spartans are said to have used a scytale for encryption and for authentication. A scytale is a cylinder with parchment wound around it. Only if somebody had a cylinder of the same diameter the receiver could read the message. That was even earlier, around 700 BC.

Today

In todays life, we have lots of 2 factor situations. Think about your Debit or credit card. You posses that card. In order to use it you need to know the related PIN you know. Only if you have both you can withdraw money from an ATM.

Think about logging into your Google, Github or many other major accounts: you can set up your accounts on those services such that somebody logging in is required to provide a PIN along the password. The PIN can for example be generated by Google Authenticator.

But there are many more identifying components that could be used. Think about you fingerprints. Or the irises in your eyes. And don’t forget your voice, though modern computer simulations can imitate it fairly well.

And lastly, there’s still SMS as a second factor out there. However, if you follow the NIST (National Institute of Standards and Technology), they recently published a draft of a Digital Authentication Guideline. In that it says:

Due to the risk that SMS messages may be intercepted or redirected, implementers of new systems SHOULD carefully consider alternative authenticators. If the out of band verification is to be made using a SMS message on a public mobile telephone network, the verifier SHALL verify that the pre-registered telephone number being used is actually associated with a mobile network and not with a VoIP (or other software-based) service. [NIST]

What they are saying is, that you should verify that the receiver of an SMS is the intended person and not somebody else interfering with the public mobile phone network.

If you’re looking into implementing 2FA, I highly recommend to give this document a read before.

The basics of 2 Factor Authentication

Let’s start with some basic, yet command and secure, processes for 2FA

HOTP

The first one, and the basis for the second one, is HOTP or HMAC based one time passwords.

HOTP sits on top of HMAC-SHA1 — which is still secure despite SHA1 being broken — and some bit and byte shifting, called truncation. The full, in-depth explanation is available in RFC 4226.

HOTP is counter based. Each time you need a new one-time password you increase a counter and get a new one-time-password. The key is know to you (or more precisely your HOTP generator) and the verifying party. In the end, the verifier has to make sure that each counter value can only be used once. That’s about it.

The following code is from the django-otp package. Slightly reformatted.

import hashlib
import hmac
import struct

def hotp(key, counter, digits=6):
    msg = struct.pack(b'>Q', counter)
    hs = hmac.new(key, msg, hashlib.sha1).digest()
    hs = list(iter(hs))

    # Truncate
    offset = hs[19] & 0x0f
    bin_code = (
        (hs[offset] & 0x7f) << 24
        | hs[offset + 1] << 16
        | hs[offset + 2] << 8
        | hs[offset + 3]
    )
    return bin_code % pow(10, digits)

TOTP

The second algorithm I pointed out before is TOTP, Time-based one-time passwords.

TOTP effectively works exactly the same as HOTP with one essential difference: the counter is derived from the current time.

What the algorithm does according to RFC RFC 6238, it takes the number of elapsed seconds between now (t) and a start (t0) and divides it by the duration of how long a single token should be valid. That defaults to 30 seconds. You effectively end up with a counter that changes every 30 seconds.

Looking at code, this is about it. Nothing fancy.

import time

def totp(key, t=None, t0=0, steps=30, digits=6):
    t = int(t or time.time())
    counter = (t - int(t0)) // int(steps)
    return hotp(key, counter, digits)

YubiKey, Nitrokey

I’m not going into details about YubiKey, Nitrokey or similar systems.

They are pieces of hardware, similar to USB sticks, that do crypto for you, for example providing HOTP tokens, or other features such as SMIME or GnuPG keys.

Django Integration

Anyway, enough about the basics you got to know before you consider 2 Factor Authentication for your project. How do you actually implement it? Here are 5 code snippets. I’ll implement a TOTP “device”, a view to enter and validate the token as well as a decorator and mixin to require a 2 factor authenticated session for a particular view.

Let us start with the models.py. Looking back at the parameters for the TOTP function, you need a key, a step, the start time and last time a token was used.

In verify_token() we check that the given token is valid by computing the current token and checking the current token was generated after the last one and matches the other token.

# models.py
import binascii
import time

from django.conf import settings
from django.db import models

from .utils import new_key, totp

class TOTPDevice(models.Model):
    # Adapted from django_otp
    user = models.OneToOneField(settings.AUTH_USER_MODEL)
    key = models.CharField(max_length=20, default=new_key)
    step = models.PositiveSmallIntegerField(default=30)
    t0 = models.BigIntegerField(default=0)
    last_t = models.BigIntegerField(default=-1)

    def verify_token(self, other):
        key = binascii.unhexlify(self.key.encode())
        t = time.time()
        token = totp(key, t, self.t0, self.step)
        if t > self.last_t and token == other:
            self.last_t = t
            self.save(update_fields=['last_t'])
            return True
        return False

For the view where we enter the token we need a form that has a single integer field and validates the token against the TOTP device for a given user.

# forms.py
from django import forms
from django.core.exceptions import ValidationError

class OTPTokenForm(forms.Form):
    token = forms.IntegerField(min_value=0, max_value=999999)

    def __init__(self, user, *args, **kwargs):
        self.user = user

    def clean(self):
        if not self.user.totpdevice.verify_token(
                self.cleaned_data['token']):
            raise ValidationError('Invalid token')

The view to enter the token checks if the session is already verified — that’s a key I defined — and redirects to some other URL if it is.

Otherwise it does normal form handling. For valid forms it updates the session and redirects too.

# views.py
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect, render
from .forms import OTPTokenForm

@login_required
def otp_token_view(request):
    if request.session.get('verified', False):
        return redirect(settings.LOGIN_REDIRECT_URL)
    if request.method == 'POST':
        form = OTPTokenForm(request.user, request.POST)
        if form.is_valid():
            request.session['verified'] = True
            return redirect(settings.LOGIN_REDIRECT_URL)
    else:
        form = OTPTokenForm(request.user)
    return render(
        request, 'otp_token_form.html', {'form': form}
    )

Now for the decorator and mixin. If the user is not authenticated redirect them to the login page. If the user is authenticated and the session has the verified flag, use the view, otherwise redirect to the 2FA view.

Warning

Mind the property behavior of is_authenticated here. That’s new in Django 1.10. If you’re on 1.9 or before you need to make it function calls.

# decorators.py
import functools
from django.conf import settings
from django.shortcuts import redirect

def is_verified(view):
    @functools.wraps(view)
    def wrapper(request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect(settings.LOGIN_URL)
        if request.session.get('verified', False):
            return view(request, *args, **kwargs)
        return redirect('otp-token-view')
    return wrapper

class IsVerified(object):
    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect(settings.LOGIN_URL)
        if request.session.get('verified', False):
            return super().dispatch(request, *args, **kwargs)
        return redirect('otp-token-view')

Well, and here’s how you finally use the decorator and mixin in views: For the IsVerified mixin, keep in mind that it has to go to the left! Mixins always go left from the base view! This extends the

# views.py
from django.http import HttpResponse
from django.views.generic import View
from .decorators import is_verified
from .mixins import IsVerified

@is_verified
def my_view(request):
    return HttpResponse('This is a 2FA function view')

class MyView(IsVerified, View):
    def get(self, request, *args, **kwargs):
        return HttpResponse('This is a 2FA class view')

Well, that was a lot of boilerplate. And there’s a bunch of stuff you don’t even have yet.

  • You don’t have a nice key setup.
  • You don’t have a QRCode view that you can scan with your phone.
  • You don’t have backup codes in case you ever lose you phone.
  • You don’t have an integration into Django Admin.

Imagine to write all that yourself for every project you want to use 2 Factor Authentication in. That’s not going to be fun. Hence there are people out there that have done most of the work for you.

django-otp provides the underlying algorithms and required database models. Feel free to use it. django-otp ships with support for email, HOTP, static tokens, TOTP. There are plug-ins for YubiKey and SMS, too. Reminder, don’t use SMS if you can avoid it. There’s also a very simplistic Django admin integration.

Another library, that I co-maintain for a few months now, is django-two- factor-auth. It heavily reuses django-otp but adds a couple of more things to the party. There’s an entire key setup process, including QR code view, backup codes. There’s a separate implementation for Django admin, not tied to django-otp. I’m also working on an advanced admin integration which includes theming and so on. If you’re staying for the sprints and are looking for something to work on, I’m happy to guide you through the process.

What’s the future of 2 Factor Authentication?

Given what I’ve talked about, where can or should you go?

In my opinion, when you deal with sensitive personal information in your projects, you probably want 2 Factor Authentication. When you deal with other people’s money, you probably want 2 Factor Authentication. When you deal with sensitive infrastructure, you probably want 2 Factor Authentication. When you deal with anything that you think might be worth it, you probably want 2 Factor Authentication. When you deal with anything else, why not have 2 Factor Authentication as an opt-in at least?

How you get there is up to you. I showed you a simple way to build the basics yourself. If that’s what you prefer, sure, please use that. If you prefer to rely on other projects, great, do it and report and contribute back as a “thank you”, please.

There’s also the option for Universal 2 Factor Authentication I mentioned in the beginning. Yuriy Ackermann gave a talk about that at Kiwi PyCon 2016.

Why?

Lastly, why would you want to offer 2FA to your customers? Kenneth Reitz recently published a post how 2FA saved a couple of accounts from being taken over.

Resources

[1]http://ancienthistory.about.com/library/bl/bl_text_polybius6.htm
[NIST]https://pages.nist.gov/800-63-3/sp800-63b.html