Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django and 2 Factor Authentication

Markus H
August 14, 2016

Django and 2 Factor Authentication

My talk from DjangoCon AU 2016

Speaker notes / blog post: https://markusholtermann.eu/2016/09/2-factor-authentication-in-django/

Markus H

August 14, 2016
Tweet

More Decks by Markus H

Other Decks in Technology

Transcript

  1. Markus Holtermann Senior Software Engineer at LaterPay Django Core Developer

    @m_holtermann • github.com/MarkusH • markusholtermann.eu
  2. EASY MICROPAYMENTS FOR YOUR FAVORITE CONTENT USE NOW, PAY LATER.

    @laterpay • github.com/laterpay • laterpay.net W e are hiring
  3. Due to the risk that SMS messages may be intercepted

    [...], implementers [...] SHOULD [...] 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 [...]. NIST — Digital Authentication Guideline — Ch 5.1.3.2. — https://pages.nist.gov/800-63-3/sp800-63b.html
  4. import hashlib, hmac, 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) https://bitbucket.org/psagers/django-otp/src/acbccb8f621b00dda 1d5018be54ab55e162586b3/django-otp/django_otp/oath.py
  5. 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)
  6. import binascii, time from django.conf import settings from django.db import

    models from .utils import new_key, totp class TOTPDevice(models.Model): 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 # Adapted from django_otp
  7. 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')
  8. 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} )
  9. 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')
  10. 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') ≥ Django 1.10
  11. 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')
  12. INSTALLED_APPS = [ 'django_otp', 'django_otp.plugins.otp_static', 'django_otp.plugins.otp_totp', 'two_factor.apps.TwoFactorConfig', 'django.contrib.admin', ... ]

    MIDDLEWARE_CLASSES = [ ... 'django_otp.middleware.OTPMiddleware', ] TWO_FACTOR_FORCE_OTP_ADMIN = True urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^', include('two_factor.urls', namespace='two_factor')), ]