Projet

Général

Profil

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

Valentin Deniaud, 29 mai 2019 15:51

Télécharger (14,3 ko)

Voir les différences:

Subject: [PATCH 2/4] auth2_multifactor: add OATH authentication factor

Add a new module auth2_multifactor, which should contain all additionnal
authentication factors.
We 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 requires the user to install a third party
app that implements TOTP, and lets them scan a QR Code, or manually
enter a key. Then they can enter a one-time password whenever required.
 MANIFEST.in                                   |  1 +
 setup.py                                      |  3 ++
 src/authentic2/auth2_multifactor/__init__.py  |  5 ++
 .../auth2_multifactor/auth_oath/__init__.py   | 10 ++++
 .../auth_oath/app_settings.py                 | 22 +++++++++
 .../auth_oath/authenticators.py               | 27 ++++++++++
 .../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     | 11 +++++
 .../auth2_multifactor/auth_oath/urls.py       |  8 +++
 .../auth2_multifactor/auth_oath/utils.py      | 38 ++++++++++++++
 .../auth2_multifactor/auth_oath/views.py      | 49 +++++++++++++++++++
 15 files changed, 240 insertions(+)
 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/app_settings.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/authenticators.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/utils.py
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/views.py
MANIFEST.in
22 22
recursive-include src/authentic2_auth_saml/templates/authentic2_auth_saml *.html
23 23
recursive-include src/authentic2_auth_oidc/templates/authentic2_auth_oidc *.html
24 24
recursive-include src/authentic2_idp_oidc/templates/authentic2_idp_oidc *.html
25
recursive-include src/authentic2/auth2_multifactor/auth_oath/templates *.html
25 26

  
26 27
recursive-include src/authentic2/saml/fixtures *.json
27 28
recursive-include src/authentic2/locale *.po *.mo
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=[
......
172 174
              'authentic2-idp-cas = authentic2_idp_cas:Plugin',
173 175
              'authentic2-idp-oidc = authentic2_idp_oidc:Plugin',
174 176
              'authentic2-provisionning-ldap = authentic2_provisionning_ldap:Plugin',
177
              'authentic2-auth-oath = authentic2.auth2_multifactor.auth_oath:Plugin',
175 178
          ],
176 179
      })
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/__init__.py
1
class Plugin(object):
2
    def get_before_urls(self):
3
        from . import urls
4
        return urls.urlpatterns
5

  
6
    def get_apps(self):
7
        return [__name__]
8

  
9
    def get_authenticators(self):
10
        return ['authentic2.auth2_multifactor.auth_oath.authenticators.TOTPAuthenticator']
src/authentic2/auth2_multifactor/auth_oath/app_settings.py
1
class AppSettings(object):
2
    __DEFAULTS = {
3
        'ENABLE': True,
4
        'LEVEL': 2,
5
    }
6

  
7
    def __init__(self, prefix):
8
        self.prefix = prefix
9

  
10
    def _setting(self, name, dflt):
11
        from django.conf import settings
12
        return getattr(settings, self.prefix+name, dflt)
13

  
14
    def __getattr__(self, name):
15
        if name not in self.__DEFAULTS:
16
            raise AttributeError(name)
17
        return self._setting(name, self.__DEFAULTS[name])
18

  
19
import sys
20
app_settings = AppSettings('A2_AUTH_OATH_')
21
app_settings.__name__ = __name__
22
sys.modules[__name__] = app_settings
src/authentic2/auth2_multifactor/auth_oath/authenticators.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 . import app_settings
7
from .views import totp_profile, totp_login
8

  
9

  
10
class TOTPAuthenticator(object):
11
    submit_name = 'oath-totp-submit'
12
    auth_level = app_settings.LEVEL
13

  
14
    def enabled(self):
15
        return app_settings.ENABLE
16

  
17
    def name(self):
18
        return ugettext_lazy('One-time password')
19

  
20
    def id(self):
21
        return 'multifactor-totp'
22

  
23
    def login(self, request, *args, **kwargs):
24
        return totp_login(request, *args, **kwargs)
25

  
26
    def profile(self, request, *args, **kwargs):
27
        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" autocomplete="off" 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
<h4>QR Code</h4>
4

  
5
<div>
6
  <p>
7
    <img src="data:image/png;base64,{{ uri }}">
8
  </p>
9
</div>
10

  
11
<a onclick="return confirm('{% trans "Warning: you will have to configure your devices again." %}')" href="{% url 'totp-change-secret' %}">{% trans "Generate new code" %}</a><br />
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, name='totp-change-secret'),
8
]
src/authentic2/auth2_multifactor/auth_oath/utils.py
1
from base64 import b64encode
2
from io import BytesIO
3
from random import SystemRandom
4

  
5
from qrcode import QRCode
6
from oath import google_authenticator
7

  
8
from .models import OATHTOTPSecret
9

  
10

  
11
def get_qrcode(user):
12
    try:
13
        secret = user.oath_totp_secret
14
    except OATHTOTPSecret.DoesNotExist:
15
        secret = set_secret(user)
16
    ga = google_authenticator.GoogleAuthenticatorURI()
17
    account = user.get_full_name().encode('utf-8')
18
    uri = ga.generate(secret.key, account=account, issuer='Publik')
19
    qr = QRCode(box_size=5)
20
    qr.add_data(uri)
21
    qr.make(fit=True)
22
    img = qr.make_image()
23
    img_bytes = BytesIO()
24
    img.save(img_bytes, format='png')
25
    uri = b64encode(img_bytes.getvalue())
26
    return uri
27

  
28

  
29
def set_secret(user):
30
    key = format(SystemRandom().getrandbits(160), '040x')
31
    secret = OATHTOTPSecret(user.id, key)
32
    secret.save()
33
    return secret
34

  
35

  
36
def get_authenticator_level():
37
    from .authenticators import TOTPAuthenticator
38
    return TOTPAuthenticator.auth_level
src/authentic2/auth2_multifactor/auth_oath/views.py
1
from django.contrib.auth.decorators import login_required
2
from django.template.loader import render_to_string
3
from django.utils.translation import ugettext as _, ugettext_lazy
4
from django.views.generic.edit import FormView
5

  
6
from oath import accept_totp
7

  
8
from authentic2.utils import redirect, get_next_url, csrf_token_check
9

  
10
from .forms import TOTPForm
11
from .utils import get_qrcode, set_secret, get_authenticator_level
12

  
13

  
14
def totp_profile(request, *args, **kwargs):
15
    context = {}
16
    context['uri'] = get_qrcode(request.user)
17
    return render_to_string('auth_oath/totp_profile.html', context, request=request)
18

  
19
class Login(FormView):
20
    template_name = 'auth_oath/totp_form.html'
21
    form_class = TOTPForm
22

  
23
    def get_context_data(self, **kwargs):
24
        kwargs['submit_name'] = 'oath-totp-submit'
25
        return super(Login, self).get_context_data(**kwargs)
26

  
27
    def get_success_url(self):
28
        return get_next_url(self.request.GET)
29

  
30
    def form_valid(self, form):
31
        code = form.cleaned_data['code']
32
        secret = self.request.user.oath_totp_secret
33
        success, drift = accept_totp(secret.key, str(code), drift=secret.drift)
34
        csrf_token_check(self.request, form)
35
        if success:
36
            secret.drift = drift
37
            secret.save()
38
            self.request.session['auth_level'] = get_authenticator_level()
39
            return super(Login, self).form_valid(form)
40
        form.add_error('code', _('Invalid code.'))
41
        return self.form_invalid(form)
42

  
43
totp_login = Login.as_view()
44

  
45

  
46
@login_required
47
def change_secret(request):
48
    set_secret(request.user)
49
    return redirect(request, 'account_management')
0
-