Projet

Général

Profil

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

Valentin Deniaud, 04 avril 2019 17:07

Télécharger (12 ko)

Voir les différences:

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
139 139
          'xstatic-select2',
140 140
          'pillow',
141 141
          'tablib',
142
          'qrcode',
143
          'oath',
142 144
      ],
143 145
      zip_safe=False,
144 146
      classifiers=[
src/authentic2/auth2_multifactor/__init__.py
1
"""
2
This package contains authentification modules that are not to be used as primary
3
modes of authentification, but rather as supplementary authentication factors in
4
order to guarantee higher levels of trust.
5
"""
src/authentic2/auth2_multifactor/auth_oath/authenticator.py
1
from django.shortcuts import render
2
from django.utils.translation import ugettext as _, ugettext_lazy
3

  
4
from oath import accept_totp
5

  
6
from authentic2 import utils
7

  
8
from .forms import TOTPForm
9
from .views import totp_profile
10

  
11

  
12
class TOTPAuthenticator(object):
13
    submit_name = 'oath-totp-submit'
14
    auth_level = 2
15

  
16
    def enabled(self):
17
        # TODO add a setting
18
        return True
19

  
20
    def name(self):
21
        return ugettext_lazy('One-time password')
22

  
23
    def id(self):
24
        return 'totp'
25

  
26
    def login(self, request, *args, **kwargs):
27
        context = kwargs.get('context', {})
28
        context['submit_name'] = self.submit_name
29
        is_post = request.method == 'POST' and self.submit_name in request.POST
30
        data = request.POST if is_post else None
31
        form = TOTPForm(data)
32
        if is_post:
33
            utils.csrf_token_check(request, form)
34
            if form.is_valid():
35
                code = form.cleaned_data['code']
36
                secret = request.user.oath_totp_secret
37
                success, drift = accept_totp(secret.key, str(code), drift=secret.drift)
38
                if success:
39
                    secret.drift = drift
40
                    secret.save()
41
                    request.session['auth_level'] = self.auth_level
42
                    return utils.continue_to_next_url(request)
43
                form.add_error('code', _('Invalid code.'))
44
        context['form'] = form
45
        return render(request, 'auth_oath/totp_form.html', context)
46

  
47
    def profile(self, request, *args, **kwargs):
48
        return totp_profile(request, *args, **kwargs)
src/authentic2/auth2_multifactor/auth_oath/forms.py
1
from django import forms
2
from django.core.exceptions import ValidationError
3
from django.utils.translation import ugettext as _
4

  
5

  
6
def validate_number(value):
7
    try:
8
        int(value)
9
    except ValueError:
10
        raise ValidationError(
11
            _('Code must be a number.'),
12
        )
13

  
14

  
15
class TOTPForm(forms.Form):
16
    code = forms.CharField(max_length=6, min_length=6,
17
                           validators=[validate_number])
src/authentic2/auth2_multifactor/auth_oath/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2019-04-01 08:21
3
from __future__ import unicode_literals
4

  
5
from django.conf import settings
6
from django.db import migrations, models
7
import django.db.models.deletion
8

  
9

  
10
class Migration(migrations.Migration):
11

  
12
    initial = True
13

  
14
    dependencies = [
15
        ('custom_user', '0016_auto_20180925_1107'),
16
    ]
17

  
18
    operations = [
19
        migrations.CreateModel(
20
            name='OATHTOTPSecret',
21
            fields=[
22
                ('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')),
23
                ('key', models.CharField(max_length=40)),
24
                ('drift', models.IntegerField(default=0)),
25
            ],
26
        ),
27
    ]
src/authentic2/auth2_multifactor/auth_oath/models.py
1
from django.conf import settings
2
from django.db import models
3
from django.utils.translation import ugettext as _
4

  
5

  
6
class OATHTOTPSecret(models.Model):
7

  
8
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True,
9
                                related_name='oath_totp_secret', verbose_name=_('user'))
10
    key = models.CharField(max_length=40)
11
    drift = models.IntegerField(default=0)
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_form.html
1
{% load i18n staticfiles %}
2
<div>
3
<form method="post" action="">
4
{% csrf_token %}
5
{{ form.as_p }}
6
<button class="submit-button" name="{{ submit_name }}">{% trans "Submit" %}</button>
7
{% if cancel %}
8
<button class="cancel-button" name="cancel">{% trans 'Cancel' %}</button>
9
{% endif %}
10
</form>
11
</div>
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
1
{% load i18n %}
2

  
3
{% comment %}
4
TODO toggle hide/show qrcode
5
add confirmation when changing it
6
dynamically update
7
add explanations
8
{% endcomment %}
9

  
10
<h4>QR Code</h4>
11

  
12
<div>
13
  <p>
14
    <img src="data:image/png;base64,{{ uri }}">
15
  </p>
16
</div>
17

  
18
<a href="/oath/change_secret">{% trans "Generate new code" %}</a>
src/authentic2/auth2_multifactor/auth_oath/urls.py
1
from django.conf.urls import url
2

  
3
from . import views
4

  
5

  
6
urlpatterns = [
7
        url(r'change_secret', views.change_secret),
8
]
src/authentic2/auth2_multifactor/auth_oath/views.py
1
from base64 import b64encode
2
from io import BytesIO
3
from random import SystemRandom
4

  
5
from django.contrib.auth.decorators import login_required
6
from django.http import HttpResponseRedirect
7
from django.shortcuts import render
8

  
9
import qrcode
10
from oath import google_authenticator
11

  
12
from .models import OATHTOTPSecret
13

  
14

  
15
def set_secret(request):
16
    key = format(SystemRandom().getrandbits(160), '040x')
17
    secret = OATHTOTPSecret(request.user.id, key)
18
    secret.save()
19
    return secret
20

  
21

  
22
def get_qrcode(request):
23
    try:
24
        secret = request.user.oath_totp_secret
25
    except OATHTOTPSecret.DoesNotExist:
26
        secret = set_secret(request)
27
    ga = google_authenticator.GoogleAuthenticatorURI()
28
    uri = ga.generate(secret.key, account=request.user.get_full_name(), issuer='Publik')
29
    qr = qrcode.make(uri)
30
    img_bytes = BytesIO()
31
    qr.save(img_bytes, format='png')  # ou save dans un httpresponse ?
32
    uri = b64encode(img_bytes.getvalue())
33
    return uri
34

  
35

  
36
def totp_profile(request, *args, **kwargs):
37
    context = {}
38
    context['uri'] = get_qrcode(request)
39
    return render(request, 'auth_oath/totp_profile.html', context)
40

  
41

  
42
@login_required
43
def change_secret(request):
44
    set_secret(request)
45
    return HttpResponseRedirect('/accounts/')
src/authentic2/auth2_multifactor/urls.py
1
from django.conf.urls import url
2

  
3

  
4
urlpatterns = [
5
        url(r'oath/', include('authentic2.auth2_multifactor.auth_oath.urls'),
6
]
src/authentic2/urls.py
27 27
    url(r'^admin/', include(admin.site.urls)),
28 28
    url(r'^idp/', include('authentic2.idp.urls')),
29 29
    url(r'^manage/', include('authentic2.manager.urls')),
30
    url(r'^api/', include('authentic2.api_urls'))
30
    url(r'^api/', include('authentic2.api_urls')),
31
    url(r'', include('authentic2.auth2_multifactor.auth_oath.urls')),
31 32
]
32 33

  
33 34

  
34
-