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