diff --git a/hiboo/account/forms.py b/hiboo/account/forms.py
index 4c05b54eb139cbac6b0fcbf10e71099d398ebefa..3a8437aec5f9652240842427e1fb7100c653931f 100644
--- a/hiboo/account/forms.py
+++ b/hiboo/account/forms.py
@@ -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):
diff --git a/hiboo/account/login.py b/hiboo/account/login.py
index 08132659b62f9597bb2d387b06e4583ab6263310..7d76bdab197b4d9059242f7d43b3e6f717f224bd 100644
--- a/hiboo/account/login.py
+++ b/hiboo/account/login.py
@@ -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)
diff --git a/hiboo/account/settings.py b/hiboo/account/settings.py
index ef92a09c5a53efed64e85c2de1e0f80e5a9c42b7..3b6383827a5f46d9ca4056300b5ae3d81d3e71a6 100644
--- a/hiboo/account/settings.py
+++ b/hiboo/account/settings.py
@@ -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")),
diff --git a/hiboo/account/templates/account_auth_totp.html b/hiboo/account/templates/account_auth_totp.html
index 19880741acfebe0b8f439e05a5feb7d277764a44..5894c414b9fd584af49722110e8e3c1f44d6288b 100644
--- a/hiboo/account/templates/account_auth_totp.html
+++ b/hiboo/account/templates/account_auth_totp.html
@@ -1,55 +1,60 @@
 {% 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 %}
diff --git a/hiboo/account/templates/account_auth_totp_enable.html b/hiboo/account/templates/account_auth_totp_enable.html
new file mode 100644
index 0000000000000000000000000000000000000000..49995d33f1dce77cce2bc7976c6f85a1db559e73
--- /dev/null
+++ b/hiboo/account/templates/account_auth_totp_enable.html
@@ -0,0 +1,34 @@
+{% 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 %}
diff --git a/hiboo/account/templates/account_signin_totp.html b/hiboo/account/templates/account_signin_totp.html
index 47a1513cac7177882e4139669fe8c9c588a1602c..9c916cb9607ac6e94650d6bf8432fb725e284a11 100644
--- a/hiboo/account/templates/account_signin_totp.html
+++ b/hiboo/account/templates/account_signin_totp.html
@@ -1,6 +1,6 @@
 {% 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 %}
diff --git a/hiboo/models.py b/hiboo/models.py
index 8ca33eeccdfb28daf72ad52f6110c50227eb0482..fd60980fcf687453a883a46dddda71ddc491a233 100644
--- a/hiboo/models.py
+++ b/hiboo/models.py
@@ -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',
diff --git a/hiboo/templates/macros.html b/hiboo/templates/macros.html
index 117e6b222d9b059e57246295db546371165fa46d..1db0c5a55b44a8e25f0098d888e0ee41e378e8d9 100644
--- a/hiboo/templates/macros.html
+++ b/hiboo/templates/macros.html
@@ -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 %}
diff --git a/hiboo/user/templates/user_details.html b/hiboo/user/templates/user_details.html
index 5e11aba5fc67135e8da94e288e2e4ae459cdb82a..7ef57b5cc15b5256e6ddacb4d4ae04a218485df6 100644
--- a/hiboo/user/templates/user_details.html
+++ b/hiboo/user/templates/user_details.html
@@ -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 %}
diff --git a/hiboo/user/views.py b/hiboo/user/views.py
index cf242338e6891b3be861c365fa6892f0f5b9da45..fb5e08805165483a36e88e7d0d3c20d38ce30344 100644
--- a/hiboo/user/views.py
+++ b/hiboo/user/views.py
@@ -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")
diff --git a/migrations/versions/f9130c1a10f7_add_enableable_auth.py b/migrations/versions/f9130c1a10f7_add_enableable_auth.py
new file mode 100644
index 0000000000000000000000000000000000000000..1724277796b2b8831f131cec259afc0ded9da517
--- /dev/null
+++ b/migrations/versions/f9130c1a10f7_add_enableable_auth.py
@@ -0,0 +1,26 @@
+""" 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')