Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • acides/hiboo
  • frju365/hiboo
  • pascoual/hiboo
  • thedarky/hiboo
  • jeremy/hiboo
  • cyrinux/hiboo
  • a.f/hiboo
  • mickge/hiboo
  • llaq/hiboo
  • vaguelysalaried/hiboo
  • felinn-glotte/hiboo
  • AntoninDelFabbro/hiboo
  • docemmetbrown/hiboo
13 results
Show changes
Commits on Source (22)
......@@ -12,8 +12,8 @@ class LoginForm(flask_wtf.FlaskForm):
class TotpForm(flask_wtf.FlaskForm):
totp = fields.PasswordField(_('Time-based One-Time Password'), [validators.DataRequired()])
submit = fields.SubmitField(_('Validate'))
totp = fields.PasswordField(_('Enter the one-time password delivered by your client'), [validators.DataRequired()])
submit = fields.SubmitField(_('Confirm'))
class SignupForm(flask_wtf.FlaskForm):
......
......@@ -3,22 +3,26 @@ from hiboo.account import blueprint, forms
from flask_babel import lazy_gettext as _
from flask import session
from authlib.jose import JsonWebToken
from io import BytesIO
import datetime
import flask_login
import flask
import pyotp
import qrcode
import base64
@blueprint.route("/signin/password", methods=["GET", "POST"])
def signin_password():
form = forms.LoginForm()
if form.validate_on_submit():
user = models.User.login(form.username.data, form.password.data)
if user and models.Auth.TOTP in user.auths:
if user and models.Auth.TOTP in user.auths and user.auths[models.Auth.TOTP].enabled:
session["username"] = user.username
return flask.redirect(utils.url_for(".signin_totp"))
elif user:
flask_login.login_user(user)
if form.remember_me.data == True:
if form.remember_me.data is True:
session.permanent = True
return flask.redirect(utils.url_or_intent(".home"))
else:
......@@ -73,7 +77,7 @@ def signup():
else:
user = models.User()
user.username = form.username.data
auth = models.Auth(models.Auth.PASSWORD)
auth = models.Auth(models.Auth.PASSWORD, enabled=True)
auth.set_password(form.password.data)
user.auths = {models.Auth.PASSWORD: auth}
models.db.session.add(user)
......@@ -113,4 +117,33 @@ def password_reset():
models.db.session.commit()
flask.flash(_("Successfully reset your password"), "success")
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"))
form = forms.LoginForm()
if form.validate_on_submit():
user = models.User.login(form.username.data, form.password.data)
if user:
flask_login.login_user(user)
if form.remember_me.data is True:
session.permanent = True
return flask.redirect(flask.url_for(".totp_disable"))
else:
flask.flash(_("Wrong credentials"), "danger")
return flask.render_template("account_signin_password.html", token=token, form=form)
......@@ -33,43 +33,61 @@ def password():
@security.authentication_required()
def totp():
user = flask_login.current_user
if models.Auth.TOTP in user.auths:
key = user.auths[models.Auth.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')
return flask.render_template(
"account_auth_totp.html",
key=key, name=user.username, issuer=issuer, qr=qr
)
return flask.render_template("account_auth_totp.html")
form = forms.TotpForm()
if form.validate_on_submit():
if user.auths[models.Auth.TOTP].check_totp(form.totp.data):
flask.flash(_("TOTP is valid"), "success")
return flask.redirect(flask.url_for(".totp"))
else:
flask.flash(_("Invalid or expired TOTP"), "danger")
return flask.redirect(flask.url_for(".totp"))
enabled = models.Auth.TOTP in user.auths and user.auths[models.Auth.TOTP].enabled
return flask.render_template("account_auth_totp.html", form=form, enabled=enabled)
@blueprint.route("/auth/totp/setup", methods=["GET", "POST"])
@blueprint.route("/auth/totp/enable", methods=["GET", "POST"])
@security.authentication_required()
@security.confirmation_required("setup TOTP")
def totp_setup():
def totp_enable():
user = flask_login.current_user
auth = models.Auth(models.Auth.TOTP)
auth.set_otp_key()
user.auths[models.Auth.TOTP] = auth
models.log(models.History.MFA, comment=str(_("TOTP has been enabled")),
user=flask_login.current_user)
models.db.session.add(auth)
models.db.session.commit()
flask.flash(_("Successfully setup TOTP"), "success")
return flask.redirect(flask.url_for(".totp"))
if models.Auth.TOTP not in user.auths:
auth = models.Auth(models.Auth.TOTP, enabled=False)
auth.set_otp_key()
user.auths[models.Auth.TOTP] = auth
models.db.session.add(auth)
models.db.session.commit()
key = user.auths[models.Auth.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')
form = forms.TotpForm()
if form.validate_on_submit():
if user.auths[models.Auth.TOTP].check_totp(form.totp.data):
models.log(models.History.MFA, comment=str(_("TOTP has been enabled")),
user=user)
user.auths[models.Auth.TOTP].enabled = True
models.db.session.add(user)
models.db.session.commit()
flask.flash(_("Successfully enabled TOTP"), "success")
return flask.redirect(flask.url_for(".totp"))
else:
flask.flash(_("Failed to enable TOTP, wrong TOTP"), "danger")
return flask.redirect(flask.url_for(".totp_enable"))
flask.flash(_("Scan this QR code or use the informations below it to configure your TOTP client"), "info")
return flask.render_template(
"account_auth_totp_enable.html",
key=key, name=user.username, issuer=issuer, qr=qr, form=form
)
@blueprint.route("/auth/totp/delete", methods=["GET", "POST"])
@blueprint.route("/auth/totp/disable", methods=["GET", "POST"])
@security.authentication_required()
@security.confirmation_required("disable TOTP")
def totp_delete():
def totp_disable():
user = flask_login.current_user
auth = user.auths[models.Auth.TOTP]
models.log(models.History.MFA, comment=str(_("TOTP has been disabled")),
......
{% extends "base.html" %}
{% block title %} {% trans %}Two-factor authentication{% endtrans %} {% endblock %}
{% block subtitle %}{% trans %}with Time-based One-Time Password (TOTP){% endtrans %}{% endblock %}
{% block title %}{% trans %}Two-factor authentication (2FA){% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}with time-based one-time password (TOTP){% endtrans %}{% endblock %}
{% block content %}
{% if not key %}
<div class="col">
<blockquote class="quote-warning">
<h5>{% trans %}Not configured{% endtrans %}</h5>
<p>{% trans %}Two-factor authentication with Time-based One-Time Passowrd is not setup.{% endtrans %}
<br>
{% trans %}Click on "Setup TOTP" to get started.{% endtrans %}
<div class="col-md-6 col">
<p>{% trans %}TOTP is an optional secondary layer of the authentication process used to enforce the protection of your account with a one-time password. You can read <a href="https://en.wikipedia.org/wiki/Time-based_one-time_password">this Wikipedia page</a> if you want to learn more about this mechanism.{% endtrans %}
</p>
</div>
{% if enabled %}
<div class="col-md-6 col">
<blockquote class="quote-success">
<h5>{% trans %}Two-factor authentication is enabled{% endtrans %}</h5>
<p>{% trans %}Click on <i>Disable TOTP</i> to disable it{% endtrans %}</p>
</p>
</blockquote>
<div class="card">
<div class="card-body">
<h5>{% trans %}Test your one-time password{% endtrans %}</h5>
<p>{% trans %}Feel free to use this form in order to check your client configuration{% endtrans %}</p>
</div>
<div class="card-footer">
{{ macros.form(form) }}
</div>
</div>
</div>
{% else %}
<blockquote class="quote-info">
<h5>{% trans %}Howto{% endtrans %}</h5>
<p>{% trans %}Scan this QR code or use text informations{% 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>
<div class="col-md-6 col">
<blockquote class="quote-info">
<h5>{% trans %}Two-factor authentication is disabled{% endtrans %}</h5>
<p>{% trans %}Click on <i>Enable TOTP</i> to configure it{% endtrans %}</p>
</blockquote>
<blockquote class="quote-warning">
<h5>{% trans %}Attention{% endtrans %}</h5>
<p>{% trans %}You will need a working TOTP client in order to complete this configuration. Several open-source apps can help you for this (and some on mobile are available on <a href="https://search.f-droid.org/?q=totp&lang=fr">F-Droid</a>){% endtrans %}</p>
</blockquote>
</div>
{% endif %}
{% endblock %}
{% block actions %}
{% if not key %}
{% if enabled %}
<a href="{{ url_for(".totp_setup") }}" class="btn btn-info">{% trans %}Setup TOTP{% endtrans %}</a>
<a href="{{ url_for(".totp_disable") }}" class="btn btn-warning">{% trans %}Disable TOTP{% endtrans %}</a>
{% else %}
<a href="{{ url_for(".totp_delete") }}" class="btn btn-warning">{% trans %}Delete TOTP{% endtrans %}</a>
<a href="{{ url_for(".totp_enable") }}" class="btn btn-info">{% trans %}Enable TOTP{% endtrans %}</a>
{% endif %}
{% endblock %}
{% extends "base.html" %}
{% block title %}{% trans %}Two-factor authentication (2FA){% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}with time-based one-time password (TOTP){% endtrans %}{% endblock %}
{% block content %}
<div class="col-md-6 col">
<div class="card">
<img class="card-img-top w-50 mx-auto" src="data:image/png;base64,{{ qr }}" alt="TOTP QR code">
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<b>{% trans %}Secret key{% endtrans %}</b> <code>{{ key }}</code>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<b>{% trans %}Name{% endtrans %}</b> <code>{{ name }}</code>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<b>{% trans %}Issuer{% endtrans %}</b> <code>{{ issuer }}</code>
</li>
</ul>
<div class="card-footer">
{{ macros.form(form) }}
</div>
</div>
</div>
{% endblock %}
{% block actions %}
<a href="{{ url_for(".totp_disable") }}" class="btn btn-warning">{% trans %}Cancel{% endtrans %}</a>
{% endblock %}
{% extends "base.html" %}
{% block title %}{% trans %}Time-based One-Time Password (TOTP) verify{% endtrans %}{% endblock %}
{% block title %}{% trans %}Time-based one-time password (TOTP) verify{% endtrans %}{% endblock %}
{% block subtitle %}{% trans %}to access your account{% endtrans %}{% endblock %}
{% block content %}
......
......@@ -164,10 +164,12 @@ class Auth(db.Model):
TOTP: "blue"
}
def __init__(self, realm):
def __init__(self, realm, enabled=False):
self.realm = realm
self.enabled = enabled
realm = db.Column(db.String(25), server_default=PASSWORD)
enabled = db.Column(db.Boolean(), nullable=False, default=1)
user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
user = db.relationship(User,
backref=db.backref('auths',
......
......@@ -82,7 +82,9 @@
<section class="content">
<div class="container-fluid">
{% for category, message in get_flashed_messages(with_categories=True) or [] %}
<div class="alert alert-{{ category or "info" }}">{{ message }}</div>
<div class="col-md-6 col">
<div class="alert alert-{{ category or "info" }}">{{ message }}</div>
</div>
{% endfor %}
{% if current_user.time_to_deletion and current_user.time_to_deletion() %}
<div class="alert alert-warning">{% trans %}Your account has no active profile, it will be deleted in{% endtrans %} {{ utils.babel.dates.format_timedelta(current_user.time_to_deletion()) }}</div>
......
......@@ -119,7 +119,7 @@
{% macro form_field(field) %}
{% if field.type == 'SubmitField' %}
{{ form_fields((field,), label=False, class="btn btn-default", **kwargs) }}
{{ form_fields((field,), label=False, class="btn btn-info", **kwargs) }}
{% elif field.type not in ('HiddenField', 'CSRFTokenField') %}
{{ form_fields((field,), **kwargs) }}
{% endif %}
......
......@@ -18,7 +18,7 @@
<dt class="col-sm-3">{% trans %}Created at{% endtrans %}</dt>
<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>
<dt class="col-sm-3">{% trans %}Auth. methods{% endtrans %}</dt>
......@@ -31,10 +31,10 @@
{% endfor %}
{% endif %}
{% if user.time_to_deletion() %}
<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>
{% endif %}
{% if user.time_to_deletion() %}
<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>
{% endif %}
</dl>
</div>
</div>
......@@ -76,4 +76,7 @@
{% block actions %}
<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 %}
......@@ -48,6 +48,25 @@ def password_reset(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"])
@security.admin_required()
@security.confirmation_required("generate a signup link")
......
""" add enableable auth
Revision ID: f9130c1a10f7
Revises: 07709c08a6d7
Create Date: 2023-02-10 14:57:20.853487
"""
from alembic import op
import sqlalchemy as sa
import hiboo
revision = 'f9130c1a10f7'
down_revision = '07709c08a6d7'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('auth') as batch_op:
batch_op.add_column(sa.Column('enabled', sa.Boolean(), server_default="1", nullable=False))
def downgrade():
with op.batch_alter_table('auth') as batch_op:
batch_op.drop_column('auth', 'enabled')