Skip to content
Snippets Groups Projects
Verified Commit 6e40df50 authored by ornanovitch's avatar ornanovitch
Browse files

Merge branch '89-activation-de-la-2fa-avec-totp-et-interface-utilisateurice'...

Merge branch '89-activation-de-la-2fa-avec-totp-et-interface-utilisateurice' of forge.tedomum.net:acides/hiboo into 89-activation-de-la-2fa-avec-totp-et-interface-utilisateurice
parents 102b0d68 874de02e
No related branches found
No related tags found
No related merge requests found
...@@ -3,10 +3,14 @@ from hiboo.account import blueprint, forms ...@@ -3,10 +3,14 @@ from hiboo.account import blueprint, forms
from flask_babel import lazy_gettext as _ from flask_babel import lazy_gettext as _
from flask import session from flask import session
from authlib.jose import JsonWebToken from authlib.jose import JsonWebToken
from io import BytesIO
import datetime import datetime
import flask_login import flask_login
import flask import flask
import pyotp
import qrcode
import base64
@blueprint.route("/signin/password", methods=["GET", "POST"]) @blueprint.route("/signin/password", methods=["GET", "POST"])
def signin_password(): def signin_password():
...@@ -113,4 +117,53 @@ def password_reset(): ...@@ -113,4 +117,53 @@ def password_reset():
models.db.session.commit() models.db.session.commit()
flask.flash(_("Successfully reset your password"), "success") flask.flash(_("Successfully reset your password"), "success")
return flask.redirect(flask.url_for(".signin_password")) return flask.redirect(flask.url_for(".signin_password"))
return flask.render_template("account_password_reset.html", form=form) return flask.render_template("account_auth_password_reset.html", form=form)
@blueprint.route("/auth/totp/reset", methods=["GET", "POST"])
def totp_reset():
token = flask.request.args.get('token') or flask.abort(403)
key = flask.current_app.config["SECRET_KEY"]
jwt = JsonWebToken(['HS512'])
claims_options = {
'exp': {'essential': True, 'value': datetime.datetime.now().timestamp()},
'aud': {'essential': True, 'value': flask.url_for('.totp_reset')},
'user_uuid': {'essential': True}
}
try:
claims = jwt.decode(token, key, claims_options=claims_options)
claims.validate()
user = models.User.query.get(claims["user_uuid"]) or flask.abort(404)
except Exception as e:
flask.flash(_("Invalid or expired reset link"), "danger")
return flask.redirect(flask.url_for(".signin_password"))
auth = user.auths[models.Auth.TOTP]
form = forms.TotpForm()
if form.validate_on_submit():
if auth.check_totp(form.totp.data):
del session["totp_reseted"]
flask.flash(_("TOTP is valid"), "success")
return flask.redirect(flask.url_for(".signin_password"))
else:
flask.flash(_("Invalid or expired TOTP, try again or disable and re-enable TOTP to reset your private key"), "danger")
return flask.redirect(flask.url_for(".totp_reset", token=token))
if "totp_reseted" not in session:
auth.set_otp_key()
models.log(models.History.MFA, comment=str(_("TOTP has been reseted")),
user=user)
models.db.session.add(auth)
models.db.session.commit()
flask.flash(_("Successfully reset TOTP key"), "success")
session["totp_reseted"] = True
key = auth.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')
return flask.render_template(
"account_auth_totp_reset.html",
key=key, name=user.username, issuer=issuer, qr=qr, form=form
)
{% extends "base.html" %}
{% block title %} {% trans %}Reset Two-factor authentication{% endtrans %} {% endblock %}
{% block subtitle %}{% trans %}Reset Time-based One-Time Password (TOTP) key{% endtrans %}{% endblock %}
{% block content %}
<blockquote class="quote-info">
<h5>{% trans %}Howto{% endtrans %}</h5>
<p>{% trans %}Scan this QR code or use text informations to configure a TOTP client{% endtrans %}</p>
</blockquote>
<div class="row">
<div class="col-md-6 col text-center">
<img src="data:image/png;base64,{{ qr }}" class="rounded mb-4" width=250 height=250>
</div>
<div class="col-md-6 col">
<ul class="list-group", style="max-width: 500px">
<li class="list-group-item d-flex justify-content-between">
{% trans %}Secret key{% endtrans %}<code>{{ key }}</code>
</li>
<li class="list-group-item d-flex justify-content-between">
{% trans %}Name{% endtrans %}<code>{{ name }}</code>
</li>
<li class="list-group-item d-flex justify-content-between">
{% trans %}Issuer{% endtrans %}<code>{{ issuer }}</code>
</li>
</ul>
</div>
{{ macros.form(form) }}
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".totp_disable") }}" class="btn btn-warning">{% trans %}Disable TOTP{% endtrans %}</a>
{% endblock %}
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<dt class="col-sm-3">{% trans %}Created at{% endtrans %}</dt> <dt class="col-sm-3">{% trans %}Created at{% endtrans %}</dt>
<dd class="col-sm-9">{{ user.created_at }}</dd> <dd class="col-sm-9">{{ user.created_at }}</dd>
<dt class="col-sm-3">{% trans %}Updated at{% endtrans %}</dt> <dt class="col-sm-3">{% trans %}Updated at{% endtrans %}</dt>
<dd class="col-sm-9">{{ user.created_at }}</dd> <dd class="col-sm-9">{{ user.created_at }}</dd>
<dt class="col-sm-3">{% trans %}Auth. methods{% endtrans %}</dt> <dt class="col-sm-3">{% trans %}Auth. methods{% endtrans %}</dt>
...@@ -31,10 +31,10 @@ ...@@ -31,10 +31,10 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if user.time_to_deletion() %} {% if user.time_to_deletion() %}
<dt class="col-sm-3">{% trans %}Deleted in{% endtrans %}</dt> <dt class="col-sm-3">{% trans %}Deleted in{% endtrans %}</dt>
<dd class="col-sm-9">{{ utils.babel.dates.format_timedelta(user.time_to_deletion()) }}</dd> <dd class="col-sm-9">{{ utils.babel.dates.format_timedelta(user.time_to_deletion()) }}</dd>
{% endif %} {% endif %}
</dl> </dl>
</div> </div>
</div> </div>
...@@ -76,4 +76,7 @@ ...@@ -76,4 +76,7 @@
{% block actions %} {% block actions %}
<a href="{{ url_for(".password_reset", user_uuid=user.uuid) }}" class="btn btn-warning">{% trans %}Password reset{% endtrans %}</a> <a href="{{ url_for(".password_reset", user_uuid=user.uuid) }}" class="btn btn-warning">{% trans %}Password reset{% endtrans %}</a>
{% if user.auths["totp"] %}
<a href="{{ url_for(".totp_reset", user_uuid=user.uuid) }}" class="btn btn-warning">{% trans %}TOTP reset{% endtrans %}</a>
{% endif %}
{% endblock %} {% endblock %}
...@@ -48,6 +48,25 @@ def password_reset(user_uuid): ...@@ -48,6 +48,25 @@ def password_reset(user_uuid):
return flask.redirect(flask.url_for(".details", user_uuid=user.uuid)) return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
@blueprint.route("/auth/totp/reset/<user_uuid>", methods=["GET", "POST"])
@security.admin_required()
@security.confirmation_required("generate a totp reset link")
def totp_reset(user_uuid):
user = models.User.query.get(user_uuid) or flask.abort(404)
expired = datetime.datetime.now() + datetime.timedelta(days=1)
payload = {
"exp": int(expired.timestamp()),
"aud": flask.url_for('account.totp_reset'),
"user_uuid": user.uuid
}
header = {"alg": "HS512"}
key = flask.current_app.config["SECRET_KEY"]
token = jwt.encode(header, payload, key)
reset_link = flask.url_for("account.totp_reset", token=token, _external=True)
flask.flash(_("Reset link: {}").format(reset_link), "success")
return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
@blueprint.route("/invite", methods=["GET", "POST"]) @blueprint.route("/invite", methods=["GET", "POST"])
@security.admin_required() @security.admin_required()
@security.confirmation_required("generate a signup link") @security.confirmation_required("generate a signup link")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment