From ab90eabfe07d0c020fa81331eb0d9f89cdf8ac45 Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Fri, 3 May 2019 11:40:10 +0200
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

diff --git a/src/authentic2/auth2_multifactor/auth_oath/authenticators.py b/src/authentic2/auth2_multifactor/auth_oath/authenticators.py
index e39c4c74..528487d7 100644
--- a/src/authentic2/auth2_multifactor/auth_oath/authenticators.py
+++ b/src/authentic2/auth2_multifactor/auth_oath/authenticators.py
@@ -10,6 +10,7 @@ from .views import totp_profile, totp_login
 class TOTPAuthenticator(object):
     submit_name = 'oath-totp-submit'
     auth_level = app_settings.LEVEL
+    _id = 'multifactor-totp'
 
     def enabled(self):
         return app_settings.ENABLE
@@ -18,7 +19,7 @@ class TOTPAuthenticator(object):
         return ugettext_lazy('One-time password')
 
     def id(self):
-        return 'multifactor-totp'
+        return self._id
 
     def login(self, request, *args, **kwargs):
         return totp_login(request, *args, **kwargs)
diff --git a/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html b/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html
new file mode 100644
index 00000000..57cb6247
--- /dev/null
+++ b/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_enable.html
@@ -0,0 +1,36 @@
+{% extends "authentic2/base-page.html" %}
+{% load i18n %}
+
+{% block content %}
+<div>
+  <p>
+    {% blocktrans %}
+      Enable this factor in order to secure authentication with a one-time
+      verification code. It will be requested at login on top of username and
+      password.
+    {% endblocktrans %}<br />
+    {% blocktrans %}
+      Install any application capable of generating such codes (refer to the
+      documentation for recommendations). Then scan the QR code below, and enter a
+      valid code in order to complete the setup.
+    {% endblocktrans %}
+  </p>
+</div>
+<div>
+  <p>
+    <img src="data:image/png;base64,{{ uri }}">
+  </p>
+  <p>
+    {% trans "For manual setup when unable to scan the code, use the following key : " %}
+    {{ secret }}
+  </p>
+</div>
+
+<div>
+<form method="post" autocomplete="off" action="">
+{% csrf_token %}
+{{ form.as_p }}
+<button class="submit-button" name="{{ submit_name }}">{% trans "Activate" %}</button>
+</form>
+</div>
+{% endblock %}
diff --git a/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html b/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
index 3ba954e7..bf3d1a02 100644
--- a/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
+++ b/src/authentic2/auth2_multifactor/auth_oath/templates/auth_oath/totp_profile.html
@@ -1,11 +1,23 @@
 {% load i18n %}
 
-<h4>QR Code</h4>
+{% if enabled %}
+  <h4>QR Code</h4>
+  <div>
+    <p>
+      <img src="data:image/png;base64,{{ uri }}">
+    </p>
+  </div>
 
-<div>
-  <p>
-    <img src="data:image/png;base64,{{ uri }}">
-  </p>
-</div>
-
-<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 />
+  <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 />
+  <a href="{% url 'totp-disable' %}">{% trans "Disable factor" %}</a>
+{% else %}
+  {% if login_url %}
+    <a href="{{ login_url }}">
+        {% trans "Insufficient authentication level to view, click here to increase." %}
+    </a>
+  {% else %}
+    <a href="{{ enable_url }}">
+        {% trans "This authentication factor is disabled, click here to set it up." %}
+    </a>
+  {% endif %}
+{% endif %}
diff --git a/src/authentic2/auth2_multifactor/auth_oath/urls.py b/src/authentic2/auth2_multifactor/auth_oath/urls.py
index 57e85137..9ca74a71 100644
--- a/src/authentic2/auth2_multifactor/auth_oath/urls.py
+++ b/src/authentic2/auth2_multifactor/auth_oath/urls.py
@@ -5,4 +5,6 @@ from . import views
 
 urlpatterns = [
     url(r'change_secret', views.change_secret, name='totp-change-secret'),
+    url(r'enable', views.enable, name='totp-enable'),
+    url(r'disable', views.disable, name='totp-disable'),
 ]
diff --git a/src/authentic2/auth2_multifactor/auth_oath/utils.py b/src/authentic2/auth2_multifactor/auth_oath/utils.py
index 2fc54086..b02c4447 100644
--- a/src/authentic2/auth2_multifactor/auth_oath/utils.py
+++ b/src/authentic2/auth2_multifactor/auth_oath/utils.py
@@ -1,4 +1,5 @@
-from base64 import b64encode
+from base64 import b64encode, b32encode
+from binascii import unhexlify
 from io import BytesIO
 from random import SystemRandom
 
@@ -33,6 +34,16 @@ def set_secret(user):
     return secret
 
 
+def get_encoded_secret(user):
+    key = user.oath_totp_secret.key
+    return b32encode(unhexlify(key)).decode('ascii')
+
+
+def get_authenticator_id():
+    from .authenticators import TOTPAuthenticator
+    return TOTPAuthenticator._id
+
+
 def get_authenticator_level():
     from .authenticators import TOTPAuthenticator
     return TOTPAuthenticator.auth_level
diff --git a/src/authentic2/auth2_multifactor/auth_oath/views.py b/src/authentic2/auth2_multifactor/auth_oath/views.py
index 07e6d3d1..a7a8a7ee 100644
--- a/src/authentic2/auth2_multifactor/auth_oath/views.py
+++ b/src/authentic2/auth2_multifactor/auth_oath/views.py
@@ -5,15 +5,38 @@ from django.views.generic.edit import FormView
 
 from oath import accept_totp
 
-from authentic2.utils import redirect, get_next_url, csrf_token_check
+from authentic2.utils import redirect, get_next_url, csrf_token_check, make_url
 
 from .forms import TOTPForm
-from .utils import get_qrcode, set_secret, get_authenticator_level
+from .utils import (get_qrcode, set_secret, get_authenticator_level,
+                    get_authenticator_id, get_encoded_secret)
 
 
 def totp_profile(request, *args, **kwargs):
     context = {}
     context['uri'] = get_qrcode(request.user)
+    try:
+        request.user.enabled_auth_factors.get(
+            authenticator_id=get_authenticator_id())
+    except request.user.enabled_auth_factors.model.DoesNotExist:
+        context['enabled'] = False
+        if 'auth_level' in request.GET:
+            # Forced enabling flow
+            context['enable_url'] = make_url('totp-enable', keep_params=True,
+                                             request=request)
+        else:
+            context['enable_url'] = make_url('totp-enable',
+                                             params={'next': request.get_full_path()})
+    else:
+        auth_level = get_authenticator_level()
+        if request.session.get('auth_level', 1) < auth_level:
+            params = {
+                'next': request.get_full_path(),
+                'auth_level': auth_level,
+            }
+            context['login_url'] = make_url('auth_login', params=params)
+        else:
+            context['enabled'] = True
     return render_to_string('auth_oath/totp_profile.html', context, request=request)
 
 class Login(FormView):
@@ -35,11 +58,14 @@ class Login(FormView):
         if success:
             secret.drift = drift
             secret.save()
-            self.request.session['auth_level'] = get_authenticator_level()
+            self.set_auth_level()
             return super(Login, self).form_valid(form)
         form.add_error('code', _('Invalid code.'))
         return self.form_invalid(form)
 
+    def set_auth_level(self):
+        self.request.session['auth_level'] = get_authenticator_level()
+
 totp_login = Login.as_view()
 
 
@@ -47,3 +73,47 @@ totp_login = Login.as_view()
 def change_secret(request):
     set_secret(request.user)
     return redirect(request, 'account_management')
+
+
+class Enable(Login):
+    """Allow the user to setup TOTP authentication.
+
+    Require a valid OTP in order to complete the process, ensuring that the
+    user setup is successful.
+    """
+    template_name = 'auth_oath/totp_enable.html'
+
+    def dispatch(self, request, *args, **kwargs):
+        try:
+            # Verify that the factor is not already enabled
+            request.user.enabled_auth_factors.get(
+                authenticator_id=get_authenticator_id())
+            return redirect(request, 'account_management')
+        except request.user.enabled_auth_factors.model.DoesNotExist:
+            return super(Enable, self).dispatch(request, *args, **kwargs)
+
+    def get_context_data(self, **kwargs):
+        kwargs['uri'] = get_qrcode(self.request.user)
+        # Also allow for manual input
+        secret = get_encoded_secret(self.request.user)
+        kwargs['secret'] = ' '.join(secret[i:i + 4]
+                                    for i in range(0, len(secret), 4))
+        return super(Enable, self).get_context_data(**kwargs)
+
+    def set_auth_level(self):
+        self.request.user.enabled_auth_factors.create(
+            authenticator_id=get_authenticator_id())
+        return super(Enable, self).set_auth_level()
+
+enable = login_required(Enable.as_view())
+
+
+@login_required
+def disable(request):
+    try:
+        factor = request.user.enabled_auth_factors.get(
+            authenticator_id=get_authenticator_id())
+        factor.delete()
+    except request.user.enabled_auth_factors.model.DoesNotExist:
+        pass
+    return redirect(request, 'account_management')
-- 
2.20.1

