Project

General

Profile

0009-auth2_multifactor-add-OATH-authentication-factor.patch

Valentin Deniaud, 04 April 2019 05:07 PM

Download (12 KB)

View differences:

Subject: [PATCH 09/13] auth2_multifactor: add OATH authentication factor

Add a new module auth2_multifactor, which should contain all
additionnal authentication factors.
We will make use of the authentication frontend/authenticator
mechanisms, for profile configuration and login blocks.
However, there is no associated backends since we will work with
users that are already authenticated.

This authentication factor works by letting the user scan a QR Code with
their phone, thus requiring a third party app that implements TOTP. Then
they can enter a one-time password when required.
 setup.py                                      |  2 +
 src/authentic2/auth2_multifactor/__init__.py  |  5 ++
 .../auth2_multifactor/auth_oath/__init__.py   |  0
 .../auth_oath/authenticator.py                | 48 +++++++++++++++++++
 .../auth2_multifactor/auth_oath/forms.py      | 17 +++++++
 .../auth_oath/migrations/0001_initial.py      | 27 +++++++++++
 .../auth_oath/migrations/__init__.py          |  0
 .../auth2_multifactor/auth_oath/models.py     | 11 +++++
 .../templates/auth_oath/totp_form.html        | 11 +++++
 .../templates/auth_oath/totp_profile.html     | 18 +++++++
 .../auth2_multifactor/auth_oath/urls.py       |  8 ++++
 .../auth2_multifactor/auth_oath/views.py      | 45 +++++++++++++++++
 src/authentic2/auth2_multifactor/urls.py      |  6 +++
 src/authentic2/urls.py                        |  3 +-
 14 files changed, 200 insertions(+), 1 deletion(-)
 create mode 100644 src/authentic2/auth2_multifactor/__init__.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/__init__.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/authenticator.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/forms.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/migrations/0001_initial.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/migrations/__init__.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/models.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_form.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/urls.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/views.py
 create mode 100644 src/authentic2/auth2_multifactor/urls.py
setup.py
'xstatic-select2',
'pillow',
'tablib',
'qrcode',
'oath',
],
zip_safe=False,
classifiers=[
src/authentic2/auth2_multifactor/__init__.py
"""
This package contains authentification modules that are not to be used as primary
modes of authentification, but rather as supplementary authentication factors in
order to guarantee higher levels of trust.
"""
src/authentic2/auth2_multifactor/auth_oath/authenticator.py
from django.shortcuts import render
from django.utils.translation import ugettext as _, ugettext_lazy
from oath import accept_totp
from authentic2 import utils
from .forms import TOTPForm
from .views import totp_profile
class TOTPAuthenticator(object):
submit_name = 'oath-totp-submit'
auth_level = 2
def enabled(self):
# TODO add a setting
return True
def name(self):
return ugettext_lazy('One-time password')
def id(self):
return 'totp'
def login(self, request, *args, **kwargs):
context = kwargs.get('context', {})
context['submit_name'] = self.submit_name
is_post = request.method == 'POST' and self.submit_name in request.POST
data = request.POST if is_post else None
form = TOTPForm(data)
if is_post:
utils.csrf_token_check(request, form)
if form.is_valid():
code = form.cleaned_data['code']
secret = request.user.oath_totp_secret
success, drift = accept_totp(secret.key, str(code), drift=secret.drift)
if success:
secret.drift = drift
secret.save()
request.session['auth_level'] = self.auth_level
return utils.continue_to_next_url(request)
form.add_error('code', _('Invalid code.'))
context['form'] = form
return render(request, 'auth_oath/totp_form.html', context)
def profile(self, request, *args, **kwargs):
return totp_profile(request, *args, **kwargs)
src/authentic2/auth2_multifactor/auth_oath/forms.py
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
def validate_number(value):
try:
int(value)
except ValueError:
raise ValidationError(
_('Code must be a number.'),
)
class TOTPForm(forms.Form):
code = forms.CharField(max_length=6, min_length=6,
validators=[validate_number])
src/authentic2/auth2_multifactor/auth_oath/migrations/0001_initial.py
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-01 08:21
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('custom_user', '0016_auto_20180925_1107'),
]
operations = [
migrations.CreateModel(
name='OATHTOTPSecret',
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='oath_totp_secret', serialize=False, to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')),
('key', models.CharField(max_length=40)),
('drift', models.IntegerField(default=0)),
],
),
]
src/authentic2/auth2_multifactor/auth_oath/models.py
from django.conf import settings
from django.db import models
from django.utils.translation import ugettext as _
class OATHTOTPSecret(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True,
related_name='oath_totp_secret', verbose_name=_('user'))
key = models.CharField(max_length=40)
drift = models.IntegerField(default=0)
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_form.html
{% load i18n staticfiles %}
<div>
<form method="post" action="">
{% csrf_token %}
{{ form.as_p }}
<button class="submit-button" name="{{ submit_name }}">{% trans "Submit" %}</button>
{% if cancel %}
<button class="cancel-button" name="cancel">{% trans 'Cancel' %}</button>
{% endif %}
</form>
</div>
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
{% load i18n %}
{% comment %}
TODO toggle hide/show qrcode
add confirmation when changing it
dynamically update
add explanations
{% endcomment %}
<h4>QR Code</h4>
<div>
<p>
<img src="data:image/png;base64,{{ uri }}">
</p>
</div>
<a href="/oath/change_secret">{% trans "Generate new code" %}</a>
src/authentic2/auth2_multifactor/auth_oath/urls.py
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'change_secret', views.change_secret),
]
src/authentic2/auth2_multifactor/auth_oath/views.py
from base64 import b64encode
from io import BytesIO
from random import SystemRandom
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect
from django.shortcuts import render
import qrcode
from oath import google_authenticator
from .models import OATHTOTPSecret
def set_secret(request):
key = format(SystemRandom().getrandbits(160), '040x')
secret = OATHTOTPSecret(request.user.id, key)
secret.save()
return secret
def get_qrcode(request):
try:
secret = request.user.oath_totp_secret
except OATHTOTPSecret.DoesNotExist:
secret = set_secret(request)
ga = google_authenticator.GoogleAuthenticatorURI()
uri = ga.generate(secret.key, account=request.user.get_full_name(), issuer='Publik')
qr = qrcode.make(uri)
img_bytes = BytesIO()
qr.save(img_bytes, format='png') # ou save dans un httpresponse ?
uri = b64encode(img_bytes.getvalue())
return uri
def totp_profile(request, *args, **kwargs):
context = {}
context['uri'] = get_qrcode(request)
return render(request, 'auth_oath/totp_profile.html', context)
@login_required
def change_secret(request):
set_secret(request)
return HttpResponseRedirect('/accounts/')
src/authentic2/auth2_multifactor/urls.py
from django.conf.urls import url
urlpatterns = [
url(r'oath/', include('authentic2.auth2_multifactor.auth_oath.urls'),
]
src/authentic2/urls.py
url(r'^admin/', include(admin.site.urls)),
url(r'^idp/', include('authentic2.idp.urls')),
url(r'^manage/', include('authentic2.manager.urls')),
url(r'^api/', include('authentic2.api_urls'))
url(r'^api/', include('authentic2.api_urls')),
url(r'', include('authentic2.auth2_multifactor.auth_oath.urls')),
]