From 951630c2a37aa0e4aba99f28a3226e35e269746a Mon Sep 17 00:00:00 2001
From: f00wl <f00wl@felinn.org>
Date: Sat, 21 Aug 2021 21:26:34 +0200
Subject: [PATCH] Add 2FA settings in user space

New routes to setup, delete and view TOTP.
Print a QR code for TOTP setup with smart phone app
Log TOTP key changes in History
Added necessary libs
---
 hiboo/account/settings.py                 | 56 +++++++++++++++++++++++
 hiboo/account/templates/account_totp.html | 25 ++++++++++
 hiboo/models.py                           |  2 +
 hiboo/templates/sidebar.html              |  5 ++
 requirements.txt                          |  2 +
 5 files changed, 90 insertions(+)
 create mode 100644 hiboo/account/templates/account_totp.html

diff --git a/hiboo/account/settings.py b/hiboo/account/settings.py
index 0b82d407..66c052d7 100644
--- a/hiboo/account/settings.py
+++ b/hiboo/account/settings.py
@@ -2,9 +2,13 @@ from hiboo.account import blueprint, forms
 from hiboo import models, security
 from wtforms import fields
 from flask_babel import lazy_gettext as _
+from io import BytesIO
 
 import flask
 import flask_login
+import pyotp
+import qrcode
+import base64
 
 
 @blueprint.route("/password", methods=["GET", "POST"])
@@ -25,6 +29,58 @@ def password():
     return flask.render_template("account_password.html", form=form)
 
 
+@blueprint.route("/totp", methods=["GET", "POST"])
+@security.authentication_required()
+def totp():
+    user = flask_login.current_user
+    infos = {}
+    if "totp" in user.auths:
+        key = user.auths["totp"].value
+        issuer = flask.current_app.config['WEBSITE_NAME']
+        totp_uri = pyotp.totp.TOTP(key).provisioning_uri(
+            name=user.username,
+            issuer_name=issuer)
+        img = qrcode.make(totp_uri).get_image()
+        buffered = BytesIO()
+        img.save(buffered, format="PNG")
+        qr = base64.b64encode(buffered.getvalue()).decode('ascii')
+        infos = {
+            "key": key,
+            "name": user.username,
+            "issuer": issuer,
+            "qr": qr
+        }
+    return flask.render_template("account_totp.html", infos=infos)
+
+
+@blueprint.route("/totp/setup", methods=["GET", "POST"])
+@security.authentication_required()
+@security.confirmation_required("Setup 2FA with TOTP")
+def totp_setup():
+    user = flask_login.current_user
+    auth = models.Auth("totp")
+    auth.set_otp_key()
+    user.auths["totp"] = auth
+    models.log(models.History.MFA, user=flask_login.current_user)
+    models.db.session.add(auth)
+    models.db.session.commit()
+    flask.flash(_("Successfully setup 2FA"), "success")
+    return flask.redirect(flask.url_for(".totp"))
+
+
+@blueprint.route("/totp/delete", methods=["GET", "POST"])
+@security.authentication_required()
+@security.confirmation_required("Delete 2FA with TOTP")
+def totp_delete():
+    user = flask_login.current_user
+    auth = user.auths["totp"]
+    models.log(models.History.MFA, user=flask_login.current_user)
+    models.db.session.delete(auth)
+    models.db.session.commit()
+    flask.flash(_("Successfully deleted 2FA"), "success")
+    return flask.redirect(flask.url_for(".totp"))
+
+
 @blueprint.route("/contact", methods=["GET", "POST"])
 @security.authentication_required()
 def contact():
diff --git a/hiboo/account/templates/account_totp.html b/hiboo/account/templates/account_totp.html
new file mode 100644
index 00000000..aad31abe
--- /dev/null
+++ b/hiboo/account/templates/account_totp.html
@@ -0,0 +1,25 @@
+{% extends "base.html" %}
+
+{% block title %}{% trans %}Two-factor authentication with Time-based One-Time Password (TOTP){% endtrans %}{% endblock %}
+
+{% block content %}
+{% if infos == {} %}
+<p>{% trans %}Two-factor authentication with Time-based One-Time Passowrd is not setup.{% endtrans %}</p>
+{% else %}
+<p>{% trans %}Scan this QR code or use text informations{% endtrans %}</p>
+<img src="data:image/png;base64,{{ infos.qr }}" width=300 height=300>
+<pre>
+{% trans %}Secret key:{% endtrans %} <code>{{ infos.key }}</code>
+{% trans %}Name:{% endtrans %} <code>{{ infos.name }}</code>
+{% trans %}Issuer:{% endtrans %} <code>{{ infos.issuer }}</code>
+</pre>
+{% endif %}
+{% endblock %}
+
+{% block actions %}
+{% if infos == {} %}
+<a href="{{ url_for(".totp_setup") }}" class="btn btn-info">{% trans %}Setup 2FA{% endtrans %}</a>
+{% else %}
+<a href="{{ url_for(".totp_delete") }}" class="btn btn-info">{% trans %}Delete 2FA{% endtrans %}</a>
+{% endif %}
+{% endblock %}
diff --git a/hiboo/models.py b/hiboo/models.py
index 9a4d4d52..ad9df0c8 100644
--- a/hiboo/models.py
+++ b/hiboo/models.py
@@ -322,11 +322,13 @@ class History(db.Model):
     STATUS = "status"
     TRANSITION = "transition"
     PASSWORD = "password"
+    MFA = "2FA"
 
     DESCRIPTION = {
         SIGNUP: _("signed up for this account"),
         CREATE: _("created the profile {this.profile.username} on {this.service.name}"),
         PASSWORD: _("changed this account password"),
+        MFA: _("alter this account two-factor authentication settings"),
         STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}"),
         TRANSITION: _("did {this.transition.label} the profile {this.profile.username} on {this.service.name}")
     }
diff --git a/hiboo/templates/sidebar.html b/hiboo/templates/sidebar.html
index 946f512f..3a1a5450 100644
--- a/hiboo/templates/sidebar.html
+++ b/hiboo/templates/sidebar.html
@@ -20,6 +20,11 @@
     <i class="nav-icon fas fa-lock"></i> <p>{% trans %}Change password{% endtrans %}</p>
   </a>
 </li>
+<li class="nav-item">
+  <a class="nav-link" href="{{ url_for("account.totp") }}">
+    <i class="nav-icon fas fa-qrcode"></i> <p>{% trans %}Two-factor authentication{% endtrans %}</p>
+  </a>
+</li>
 <li class="nav-item">
   <a class="nav-link" href="{{ url_for("account.signout") }}">
     <i class="nav-icon fas fa-sign-out-alt"></i> <p>{% trans %}Sign out{% endtrans %}</p>
diff --git a/requirements.txt b/requirements.txt
index 50ec145a..a33fa0a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,4 +25,6 @@ terminaltables
 Werkzeug==0.16.0
 Pillow
 email_validator
+pyotp
+qrcode
 git+https://forge.tedomum.net/tedomum/axon.git
-- 
GitLab