Projet

Général

Profil

0003-auth_oath-allow-user-to-enable-OATH-factor.patch

Valentin Deniaud, 29 mai 2019 16:06

Télécharger (10 ko)

Voir les différences:

Subject: [PATCH 1/2] auth_oath: allow user to enable OATH factor

Be careful that when a user has enabled an authentification factor, they
must not be allowed configuration to it anymore without an
authentication level at least equal to the one provided by said factor.
 .../auth_oath/authenticators.py               |  3 +-
 .../templates/auth_oath/totp_enable.html      | 36 +++++++++
 .../templates/auth_oath/totp_profile.html     | 28 +++++--
 .../auth2_multifactor/auth_oath/urls.py       |  2 +
 .../auth2_multifactor/auth_oath/utils.py      | 13 +++-
 .../auth2_multifactor/auth_oath/views.py      | 76 ++++++++++++++++++-
 6 files changed, 145 insertions(+), 13 deletions(-)
 create mode 100644 src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html
src/authentic2/auth2_multifactor/auth_oath/authenticators.py
10 10
class TOTPAuthenticator(object):
11 11
    submit_name = 'oath-totp-submit'
12 12
    auth_level = app_settings.LEVEL
13
    _id = 'multifactor-totp'
13 14

  
14 15
    def enabled(self):
15 16
        return app_settings.ENABLE
......
18 19
        return ugettext_lazy('One-time password')
19 20

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

  
23 24
    def login(self, request, *args, **kwargs):
24 25
        return totp_login(request, *args, **kwargs)
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
      Enable this factor in order to secure authentication with a one-time
9
      verification code. It will be requested at login on top of username and
10
      password.
11
    {% endblocktrans %}<br />
12
    {% blocktrans %}
13
      Install any application capable of generating such codes (refer to the
14
      documentation for recommendations). Then scan the QR code below, and enter a
15
      valid code in order to complete the setup.
16
    {% endblocktrans %}
17
  </p>
18
</div>
19
<div>
20
  <p>
21
    <img src="data:image/png;base64,{{ uri }}">
22
  </p>
23
  <p>
24
    {% trans "For manual setup when unable to scan the code, use the following key : " %}
25
    {{ secret }}
26
  </p>
27
</div>
28

  
29
<div>
30
<form method="post" autocomplete="off" action="">
31
{% csrf_token %}
32
{{ form.as_p }}
33
<button class="submit-button" name="{{ submit_name }}">{% trans "Activate" %}</button>
34
</form>
35
</div>
36
{% endblock %}
src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
1 1
{% load i18n %}
2 2

  
3
<h4>QR Code</h4>
3
{% if enabled %}
4
  <h4>QR Code</h4>
5
  <div>
6
    <p>
7
      <img src="data:image/png;base64,{{ uri }}">
8
    </p>
9
  </div>
4 10

  
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 />
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 />
12
  <a href="{% url 'totp-disable' %}">{% trans "Disable factor" %}</a>
13
{% else %}
14
  {% if login_url %}
15
    <a href="{{ login_url }}">
16
        {% trans "Insufficient authentication level to view, click here to increase." %}
17
    </a>
18
  {% else %}
19
    <a href="{{ enable_url }}">
20
        {% trans "This authentication factor is disabled, click here to set it up." %}
21
    </a>
22
  {% endif %}
23
{% endif %}
src/authentic2/auth2_multifactor/auth_oath/urls.py
5 5

  
6 6
urlpatterns = [
7 7
    url(r'change_secret', views.change_secret, name='totp-change-secret'),
8
    url(r'enable', views.enable, name='totp-enable'),
9
    url(r'disable', views.disable, name='totp-disable'),
8 10
]
src/authentic2/auth2_multifactor/auth_oath/utils.py
1
from base64 import b64encode
1
from base64 import b64encode, b32encode
2
from binascii import unhexlify
2 3
from io import BytesIO
3 4
from random import SystemRandom
4 5

  
......
33 34
    return secret
34 35

  
35 36

  
37
def get_encoded_secret(user):
38
    key = user.oath_totp_secret.key
39
    return b32encode(unhexlify(key)).decode('ascii')
40

  
41

  
42
def get_authenticator_id():
43
    from .authenticators import TOTPAuthenticator
44
    return TOTPAuthenticator._id
45

  
46

  
36 47
def get_authenticator_level():
37 48
    from .authenticators import TOTPAuthenticator
38 49
    return TOTPAuthenticator.auth_level
src/authentic2/auth2_multifactor/auth_oath/views.py
5 5

  
6 6
from oath import accept_totp
7 7

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

  
10 10
from .forms import TOTPForm
11
from .utils import get_qrcode, set_secret, get_authenticator_level
11
from .utils import (get_qrcode, set_secret, get_authenticator_level,
12
                    get_authenticator_id, get_encoded_secret)
12 13

  
13 14

  
14 15
def totp_profile(request, *args, **kwargs):
15 16
    context = {}
16 17
    context['uri'] = get_qrcode(request.user)
18
    try:
19
        request.user.enabled_auth_factors.get(
20
            authenticator_id=get_authenticator_id())
21
    except request.user.enabled_auth_factors.model.DoesNotExist:
22
        context['enabled'] = False
23
        if 'auth_level' in request.GET:
24
            # Forced enabling flow
25
            context['enable_url'] = make_url('totp-enable', keep_params=True,
26
                                             request=request)
27
        else:
28
            context['enable_url'] = make_url('totp-enable',
29
                                             params={'next': request.get_full_path()})
30
    else:
31
        auth_level = get_authenticator_level()
32
        if request.session.get('auth_level', 1) < auth_level:
33
            params = {
34
                'next': request.get_full_path(),
35
                'auth_level': auth_level,
36
            }
37
            context['login_url'] = make_url('auth_login', params=params)
38
        else:
39
            context['enabled'] = True
17 40
    return render_to_string('auth_oath/totp_profile.html', context, request=request)
18 41

  
19 42
class Login(FormView):
......
35 58
        if success:
36 59
            secret.drift = drift
37 60
            secret.save()
38
            self.request.session['auth_level'] = get_authenticator_level()
61
            self.set_auth_level()
39 62
            return super(Login, self).form_valid(form)
40 63
        form.add_error('code', _('Invalid code.'))
41 64
        return self.form_invalid(form)
42 65

  
66
    def set_auth_level(self):
67
        self.request.session['auth_level'] = get_authenticator_level()
68

  
43 69
totp_login = Login.as_view()
44 70

  
45 71

  
......
47 73
def change_secret(request):
48 74
    set_secret(request.user)
49 75
    return redirect(request, 'account_management')
76

  
77

  
78
class Enable(Login):
79
    """Allow the user to setup TOTP authentication.
80

  
81
    Require a valid OTP in order to complete the process, ensuring that the
82
    user setup is successful.
83
    """
84
    template_name = 'auth_oath/totp_enable.html'
85

  
86
    def dispatch(self, request, *args, **kwargs):
87
        try:
88
            # Verify that the factor is not already enabled
89
            request.user.enabled_auth_factors.get(
90
                authenticator_id=get_authenticator_id())
91
            return redirect(request, 'account_management')
92
        except request.user.enabled_auth_factors.model.DoesNotExist:
93
            return super(Enable, self).dispatch(request, *args, **kwargs)
94

  
95
    def get_context_data(self, **kwargs):
96
        kwargs['uri'] = get_qrcode(self.request.user)
97
        # Also allow for manual input
98
        secret = get_encoded_secret(self.request.user)
99
        kwargs['secret'] = ' '.join(secret[i:i + 4]
100
                                    for i in range(0, len(secret), 4))
101
        return super(Enable, self).get_context_data(**kwargs)
102

  
103
    def set_auth_level(self):
104
        self.request.user.enabled_auth_factors.create(
105
            authenticator_id=get_authenticator_id())
106
        return super(Enable, self).set_auth_level()
107

  
108
enable = login_required(Enable.as_view())
109

  
110

  
111
@login_required
112
def disable(request):
113
    try:
114
        factor = request.user.enabled_auth_factors.get(
115
            authenticator_id=get_authenticator_id())
116
        factor.delete()
117
    except request.user.enabled_auth_factors.model.DoesNotExist:
118
        pass
119
    return redirect(request, 'account_management')
50
-