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