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