Projet

Général

Profil

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

Valentin Deniaud, 11 juillet 2019 16:40

Télécharger (29,9 ko)

Voir les différences:

Subject: [PATCH] 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   |  15 ++
 .../auth_oath/app_settings.py                 |  23 ++
 .../auth_oath/authenticators.py               |  26 +++
 .../auth2_multifactor/auth_oath/forms.py      |  35 +++
 .../auth_oath/migrations/0001_initial.py      |  36 +++
 .../auth_oath/migrations/__init__.py          |   0
 .../auth2_multifactor/auth_oath/models.py     |  36 +++
 .../templates/auth_oath/disable_confirm.html  |  16 ++
 .../templates/auth_oath/generate_confirm.html |  10 +
 .../auth_oath/templates/auth_oath/qrcode.html |   3 +
 .../templates/auth_oath/totp_enable.html      |  30 +++
 .../templates/auth_oath/totp_form.html        |  23 ++
 .../templates/auth_oath/totp_profile.html     |  24 ++
 .../auth_oath/totp_recovery_display.html      |  22 ++
 .../auth_oath/totp_recovery_form.html         |   6 +
 .../auth2_multifactor/auth_oath/urls.py       |  22 ++
 .../auth2_multifactor/auth_oath/utils.py      |  21 ++
 .../auth2_multifactor/auth_oath/views.py      | 208 ++++++++++++++++++
 .../auth2_multifactor/decorators.py           |  11 +
 22 files changed, 576 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/disable_confirm.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/generate_confirm.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/qrcode.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html
 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/templates/auth_oath/totp_recovery_display.html
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_recovery_form.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
 create mode 100644 src/authentic2/auth2_multifactor/decorators.py
MANIFEST.in
25 25
recursive-include src/authentic2_auth_saml/templates/authentic2_auth_saml *.html
26 26
recursive-include src/authentic2_auth_oidc/templates/authentic2_auth_oidc *.html
27 27
recursive-include src/authentic2_idp_oidc/templates/authentic2_idp_oidc *.html
28
recursive-include src/authentic2/auth2_multifactor/auth_oath/templates *.html
28 29

  
29 30
recursive-include src/authentic2/saml/fixtures *.json
30 31
recursive-include src/authentic2/locale *.po *.mo
setup.py
140 140
          'xstatic-select2',
141 141
          'pillow',
142 142
          'tablib',
143
          'qrcode',
144
          'oath',
143 145
      ],
144 146
      zip_safe=False,
145 147
      classifiers=[
......
174 176
              'authentic2-idp-oidc = authentic2_idp_oidc:Plugin',
175 177
              'authentic2-provisionning-ldap = authentic2_provisionning_ldap:Plugin',
176 178
              'authentic2-auth-fc = authentic2_auth_fc:Plugin',
179
              'authentic2-auth-oath = authentic2.auth2_multifactor.auth_oath:Plugin',
177 180
          ],
178 181
      })
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 app_settings
4
        from django.conf.urls import include, url
5
        from authentic2.decorators import setting_enabled, required
6

  
7
        return required(
8
            setting_enabled('ENABLE', settings=app_settings),
9
            [url(r'^accounts/authenticators/totp/', include(__name__ + '.urls'))])
10

  
11
    def get_apps(self):
12
        return [__name__]
13

  
14
    def get_authenticators(self):
15
        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

  
20
import sys
21
app_settings = AppSettings('A2_AUTH_OATH_')
22
app_settings.__name__ = __name__
23
sys.modules[__name__] = app_settings
src/authentic2/auth2_multifactor/auth_oath/authenticators.py
1
from django.utils.translation import ugettext_lazy
2

  
3
from . import app_settings
4

  
5

  
6
class TOTPAuthenticator(object):
7
    submit_name = 'oath-totp-submit'
8
    auth_level = app_settings.LEVEL
9
    _id = 'multifactor-totp'
10

  
11
    def enabled(self):
12
        return app_settings.ENABLE
13

  
14
    def name(self):
15
        return ugettext_lazy('One-time password')
16

  
17
    def id(self):
18
        return self._id
19

  
20
    def login(self, request, *args, **kwargs):
21
        from .views import totp_login
22
        return totp_login(request, *args, **kwargs)
23

  
24
    def profile(self, request, *args, **kwargs):
25
        from .views import totp_profile
26
        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 EnableForm(forms.Form):
16
    code = forms.CharField(
17
        max_length=6, min_length=6, validators=[validate_number],
18
        label=_('Code'),
19
    )
20

  
21

  
22
class LoginForm(EnableForm):
23
    def __init__(self, *args, **kwargs):
24
        super(LoginForm, self).__init__(*args, **kwargs)
25
        self.fields['code'].help_text = \
26
            _('In order to be granted access, you must enter the six-digit '
27
              'code given by the authentificator app on your device.')
28

  
29

  
30
class RecoveryForm(forms.Form):
31
    code = forms.CharField(
32
        max_length=10, min_length=10, label=_('Recovery code'),
33
        help_text=_("Use one of your recovery code. Remember these are for one-time "
34
                    "use, hence the code you choose won't be valid next time.")
35
    )
src/authentic2/auth2_multifactor/auth_oath/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2019-07-11 14:39
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
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
16
        ('custom_user', '0016_auto_20180925_1107'),
17
    ]
18

  
19
    operations = [
20
        migrations.CreateModel(
21
            name='OATHTOTPSecret',
22
            fields=[
23
                ('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')),
24
                ('key', models.CharField(max_length=40)),
25
                ('drift', models.IntegerField(default=0)),
26
            ],
27
        ),
28
        migrations.CreateModel(
29
            name='RecoveryCode',
30
            fields=[
31
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
32
                ('code', models.CharField(max_length=10)),
33
                ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='totp_recovery_codes', to=settings.AUTH_USER_MODEL, verbose_name='utilisateur')),
34
            ],
35
        ),
36
    ]
src/authentic2/auth2_multifactor/auth_oath/models.py
1
from base64 import b32encode
2
from binascii import unhexlify
3
from random import SystemRandom
4

  
5
from django.conf import settings
6
from django.db import models
7
from django.utils.translation import ugettext as _
8

  
9

  
10
class OATHTOTPSecret(models.Model):
11

  
12
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True,
13
                                related_name='oath_totp_secret', verbose_name=_('user'))
14
    key = models.CharField(max_length=40)
15
    drift = models.IntegerField(default=0)
16

  
17
    def b32_encoded(self):
18
        return b32encode(unhexlify(self.key)).decode('ascii')
19

  
20

  
21
class RecoveryCodeManager(models.Manager):
22
    # j'aurais bien aime avoir un truc qui permette de faire
23
    # user.totp_recovery_codes.generate() mais j'ai pas reussi, relecteur si tu
24
    # m'entends...
25
    def generate_for_user(self, user):
26
        for _ in range(10):
27
            code = format(SystemRandom().getrandbits(40), '010x')
28
            self.create(user=user, code=code)
29

  
30

  
31
class RecoveryCode(models.Model):
32
    objects = RecoveryCodeManager()
33

  
34
    user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='totp_recovery_codes',
35
                             verbose_name=_('user'))
36
    code = models.CharField(max_length=10)
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/disable_confirm.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
  <form method="post">
6
    {% csrf_token %}
7
    <p>{% trans "Are you sure you want to disable the one-time password authenticator?" %}</p>
8
    <input type="submit" value="{% trans "Confirm" %}">
9
  </form>
10

  
11
  {% blocktrans %}
12
    Doing so will invalidate configuration on all devices, meaning that if
13
    it is enabled again the codes they generate won't be valid anymore.
14
    Recovery codes will also be discarded.
15
  {% endblocktrans %}
16
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/generate_confirm.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
  <form method="post">
6
    {% csrf_token %}
7
    <p>{% trans "Generating new recovery codes will invalidate old ones." %}</p>
8
    <input type="submit" value="{% trans "Confirm" %}">
9
  </form>
10
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/qrcode.html
1
<div style="text-align: center">
2
  <img src="data:image/png;base64,{{ uri }}">
3
</div>
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<div>
6
  <p>
7
    {% blocktrans %}
8
      Install any application capable of generating such codes (refer to the
9
      documentation for recommendations). Then scan the QR code below, and
10
      enter the six-digit code that the app will display in order to complete
11
      the setup.
12
    {% endblocktrans %}
13
  </p>
14
</div>
15
<div>
16
  {% include "auth_oath/qrcode.html" %}
17
  <p>
18
    {% trans "For manual setup when unable to scan the code, enter the following key : " %}
19
    {{ secret }}
20
  </p>
21
</div>
22

  
23
<div>
24
  <form method="post" autocomplete="off" action="">
25
    {% csrf_token %}
26
    {{ form.as_p }}
27
    <button class="submit-button" name="{{ submit_name }}">{% trans "Activate" %}</button>
28
  </form>
29
</div>
30
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_form.html
1
{% load i18n %}
2

  
3
<h2>{% trans "Multi-factor authentication" %}</h1>
4

  
5
<div>
6
  <form method="post" autocomplete="off" action="">
7
    {% csrf_token %}
8
    {{ form.as_p }}
9
    <button class="submit-button" name="{{ submit_name }}">{% trans "Submit" %}</button>
10
    {% if cancel %}
11
      <button class="cancel-button" name="cancel">{% trans 'Cancel' %}</button>
12
    {% endif %}
13
  </form>
14
</div>
15

  
16
{% if recovery_url %}
17
  <div>
18
    <p>
19
      {% trans "If missing your device, " %}
20
      <a href="{{ recovery_url }}">{% trans "click here to use a recovery code." %}</a>
21
    </p>
22
  </div>
23
{% endif %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
1
{% load i18n %}
2

  
3
{% if enabled %}
4
  <p>
5
    {% trans "You can configure additional devices by scanning the QR code below." %}
6
  </p>
7
  {% include "auth_oath/qrcode.html" %}
8
  <a href={% url 'totp-recovery-generate' %}>{% trans "Generate new recovery codes" %}</a><br />
9
  <a href="{% url 'totp-disable' %}">{% trans "Disable this factor" %}</a>
10
{% else %}
11
  {% if login_url %}
12
    <a href="{{ login_url }}">
13
      {% trans "Insufficient authentication level to view, click here to increase." %}
14
    </a>
15
  {% else %}
16
    <a href="{{ enable_url }}">
17
      {% trans "Enable this factor " %}
18
    </a>
19
    {% blocktrans %}
20
      in order to secure authentication with a one-time verification code. It
21
      will be requested when needed on top of username and password.
22
    {% endblocktrans %}
23
  {% endif %}
24
{% endif %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_recovery_display.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
  <h2>Recovery codes</h2>
6
  <p>
7
  {% blocktrans %}
8
    Please store the following one-time codes in a safe place. If you ever lose
9
    your device, they can be used to pass the verification step and regain
10
    access to your account.
11
  {% endblocktrans %}
12
  <ul>
13
    {% for code in object_list %}
14
      <li>{{ code.code }}</li>
15
    {% endfor %}
16
  </ul>
17
  {% if next_url %}
18
    <a href="{{ next_url }}">{% trans "Continue" %}</a>
19
  {% else %}
20
    <a href="{% url 'authenticators_profile' %}">{% trans "Back" %}</a>
21
  {% endif %}
22
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_recovery_form.html
1
{% extends "authentic2/base-page.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
  {% include "auth_oath/totp_form.html" %}
6
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/urls.py
1
from django.conf.urls import url
2
from django.contrib.auth.decorators import login_required
3

  
4
from authentic2.decorators import required
5
from authentic2.auth2_multifactor.decorators import auth_level_required
6

  
7
from . import views
8
from .authenticators import TOTPAuthenticator
9

  
10

  
11
urlpatterns = required(
12
    (login_required, auth_level_required(TOTPAuthenticator.auth_level)), [
13
        url(r'^disable/$', views.disable, name='totp-disable'),
14
        url(r'^recovery/codes/$', views.recovery_display, name='totp-recovery-display'),
15
        url(r'^recovery/generate/$', views.recovery_generate, name='totp-recovery-generate'),
16
    ]
17
)
18

  
19
urlpatterns += [
20
    url(r'^enable/$', login_required(views.enable), name='totp-enable'),
21
    url(r'^recovery/$', login_required(views.recovery), name='totp-recovery'),
22
]
src/authentic2/auth2_multifactor/auth_oath/utils.py
1
from base64 import b64encode
2
from io import BytesIO
3

  
4
from qrcode import QRCode
5
from oath import google_authenticator
6

  
7

  
8
def make_qrcode(user, secret=None):
9
    if not secret:
10
        secret = user.oath_totp_secret
11
    ga = google_authenticator.GoogleAuthenticatorURI()
12
    account = user.get_full_name().encode('utf-8')
13
    uri = ga.generate(secret.key, account=account, issuer='Publik')
14
    qr = QRCode(box_size=5)
15
    qr.add_data(uri)
16
    qr.make(fit=True)
17
    img = qr.make_image()
18
    img_bytes = BytesIO()
19
    img.save(img_bytes, format='png')
20
    uri = b64encode(img_bytes.getvalue())
21
    return uri
src/authentic2/auth2_multifactor/auth_oath/views.py
1
from random import SystemRandom
2

  
3
from django.template.loader import render_to_string
4
from django.utils.translation import ugettext as _
5
from django.views.generic import TemplateView
6
from django.views.generic.list import ListView
7
from django.views.generic.edit import FormView
8

  
9
from oath import accept_totp
10

  
11
from authentic2.utils import redirect, get_next_url, csrf_token_check, make_url
12

  
13
from .authenticators import TOTPAuthenticator
14
from .forms import LoginForm, EnableForm, RecoveryForm
15
from .models import OATHTOTPSecret, RecoveryCode
16
from .utils import make_qrcode
17

  
18
RECOVERY_DISPLAY = 'allow_totp_recovery_code_display'
19

  
20

  
21
class Profile(TemplateView):
22
    template_name = 'auth_oath/totp_profile.html'
23

  
24
    def get_context_data(self, **context):
25
        try:
26
            self.request.user.enabled_auth_factors.get(
27
                authenticator_id=TOTPAuthenticator._id)
28
        except self.request.user.enabled_auth_factors.model.DoesNotExist:
29
            context['enabled'] = False
30
            if 'auth_level' in self.request.GET:
31
                # Forced enabling flow
32
                context['enable_url'] = make_url('totp-enable', keep_params=True,
33
                                                 request=self.request)
34
            else:
35
                context['enable_url'] = make_url('totp-enable',
36
                                                 params={'next': self.request.get_full_path()})
37
        else:
38
            context['uri'] = make_qrcode(self.request.user)
39
            auth_level = TOTPAuthenticator.auth_level
40
            if self.request.session.get('auth_level', 1) < auth_level:
41
                params = {
42
                    'next': self.request.get_full_path(),
43
                    'auth_level': auth_level,
44
                }
45
                context['login_url'] = make_url('auth_login', params=params)
46
            else:
47
                context['enabled'] = True
48
        return super(Profile, self).get_context_data(**context)
49

  
50

  
51
totp_profile = Profile.as_view()
52

  
53

  
54
class Login(FormView):
55
    template_name = 'auth_oath/totp_form.html'
56
    form_class = LoginForm
57

  
58
    def get_context_data(self, **kwargs):
59
        kwargs['submit_name'] = 'oath-totp-submit'
60
        kwargs['recovery_url'] = make_url('totp-recovery', keep_params=True,
61
                                          request=self.request)
62
        return super(Login, self).get_context_data(**kwargs)
63

  
64
    def get_success_url(self):
65
        return get_next_url(self.request.GET)
66

  
67
    def form_valid(self, form):
68
        code = form.cleaned_data['code']
69
        secret = self.request.user.oath_totp_secret
70
        success, drift = accept_totp(secret.key, str(code), drift=secret.drift)
71
        csrf_token_check(self.request, form)
72
        if success:
73
            secret.drift = drift
74
            secret.save()
75
            self.request.session['auth_level'] = TOTPAuthenticator.auth_level
76
            return super(Login, self).form_valid(form)
77
        form.add_error('code', _('Invalid code.'))
78
        return self.form_invalid(form)
79

  
80

  
81
totp_login = Login.as_view()
82

  
83

  
84
class Enable(FormView):
85
    template_name = 'auth_oath/totp_enable.html'
86
    form_class = EnableForm
87

  
88
    def dispatch(self, request, *args, **kwargs):
89
        try:
90
            request.user.enabled_auth_factors.get(
91
                authenticator_id=TOTPAuthenticator._id)
92
            return redirect(request, 'authenticators_profile')
93
        except request.user.enabled_auth_factors.model.DoesNotExist:
94
            return super(Enable, self).dispatch(request, *args, **kwargs)
95

  
96
    def get_success_url(self):
97
        self.request.session[RECOVERY_DISPLAY] = True
98
        return make_url('totp-recovery-display', keep_params=True,
99
                        request=self.request)
100

  
101
    def get_context_data(self, **kwargs):
102
        secret = self.get_secret_for_session()
103
        kwargs['uri'] = make_qrcode(self.request.user, secret)
104
        encoded_secret = secret.b32_encoded()
105
        kwargs['secret'] = ' '.join(encoded_secret[i:i + 4]
106
                                    for i in range(0, len(encoded_secret), 4))
107
        return super(Enable, self).get_context_data(**kwargs)
108

  
109
    def get_secret_for_session(self):
110
        """Make sure a new temporary secret is generated each session.
111

  
112
        We don't want to generate a secret on every page refresh.
113
        On the other hand, permanently storing the secret before the user has
114
        completed setup would allow an attacker who already owns the account to
115
        preventively get it, so that in case the user decides to enable TOTP
116
        they would still have access.
117
        """
118
        key = self.request.session.setdefault(
119
            'oath_totp_secret', format(SystemRandom().getrandbits(160), '040x'))
120
        return OATHTOTPSecret(self.request.user.id, key)
121

  
122
    def form_valid(self, form):
123
        code = form.cleaned_data['code']
124
        secret = self.get_secret_for_session()
125
        success, _ = accept_totp(secret.key, str(code))
126
        if success:
127
            secret.save()
128
            self.request.user.enabled_auth_factors.create(
129
                authenticator_id=TOTPAuthenticator._id)
130
            self.request.session['auth_level'] = TOTPAuthenticator.auth_level
131
            del self.request.session['oath_totp_secret']
132
            RecoveryCode.objects.generate_for_user(self.request.user)
133
            return super(Enable, self).form_valid(form)
134
        form.add_error('code', _('Invalid code.'))
135
        return self.form_invalid(form)
136

  
137

  
138
enable = Enable.as_view()
139

  
140

  
141
class Disable(TemplateView):
142
    template_name = 'auth_oath/disable_confirm.html'
143

  
144
    def post(self, request):
145
        try:
146
            factor = request.user.enabled_auth_factors.get(
147
                authenticator_id=TOTPAuthenticator._id)
148
            factor.delete()
149
        except request.user.enabled_auth_factors.model.DoesNotExist:
150
            pass
151
        else:
152
            request.user.oath_totp_secret.delete()
153
            request.user.totp_recovery_codes.all().delete()
154
        return redirect(request, 'authenticators_profile')
155

  
156

  
157
disable = Disable.as_view()
158

  
159

  
160
class RecoveryCodesDisplay(ListView):
161
    template_name = 'auth_oath/totp_recovery_display.html'
162

  
163
    def get_context_data(self, **kwargs):
164
        kwargs['next_url'] = get_next_url(self.request.GET)
165
        return super(RecoveryCodesDisplay, self).get_context_data(**kwargs)
166

  
167
    def get_queryset(self):
168
        # Recovery codes should be displayed only once after generation
169
        if self.request.session.get(RECOVERY_DISPLAY):
170
            return self.request.user.totp_recovery_codes.all()
171

  
172

  
173
recovery_display = RecoveryCodesDisplay.as_view()
174

  
175

  
176
class RecoveryFormView(FormView):
177
    template_name = 'auth_oath/totp_recovery_form.html'
178
    form_class = RecoveryForm
179

  
180
    def get_success_url(self):
181
        return get_next_url(self.request.GET)
182

  
183
    def form_valid(self, form):
184
        code = form.cleaned_data['code']
185
        try:
186
            recovery_code = self.request.user.totp_recovery_codes.get(code=code)
187
        except self.request.user.totp_recovery_codes.model.DoesNotExist:
188
            form.add_error('code', _('Invalid code.'))
189
            return self.form_invalid(form)
190
        recovery_code.delete()
191
        self.request.session['auth_level'] = TOTPAuthenticator.auth_level
192
        return super(RecoveryFormView, self).form_valid(form)
193

  
194

  
195
recovery = RecoveryFormView.as_view()
196

  
197

  
198
class RecoveryGenerate(TemplateView):
199
    template_name = 'auth_oath/generate_confirm.html'
200

  
201
    def post(self, request):
202
        request.user.totp_recovery_codes.all().delete()
203
        RecoveryCode.objects.generate_for_user(request.user)
204
        request.session[RECOVERY_DISPLAY] = True
205
        return redirect(request, 'totp-recovery-display')
206

  
207

  
208
recovery_generate = RecoveryGenerate.as_view()
src/authentic2/auth2_multifactor/decorators.py
1
from django.core.exceptions import PermissionDenied
2

  
3

  
4
def auth_level_required(auth_level):
5
    def actual_decorator(func):
6
        def wrapped(request, *args, **kwargs):
7
            if request.session.get('auth_level', 1) < auth_level:
8
                raise PermissionDenied
9
            return func(request, *args, **kwargs)
10
        return wrapped
11
    return actual_decorator
0
-