0009-auth2_multifactor-add-OATH-authentication-factor.patch
| 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')),
|
||
|
]
|
||