0002-auth2_multifactor-add-OATH-authentication-factor.patch
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 |
- |