From bac63e63f8232fb79a044f66d31d38c848c1c64a Mon Sep 17 00:00:00 2001
From: f00wl <f00wl@felinn.org>
Date: Sun, 4 Dec 2022 17:25:24 +0100
Subject: [PATCH] rewrite password reset mechanism with JWT * create a JWT
 instead of reset_token db entry in view * rewrite reset route with JWT based
 verification

---
 hiboo/account/login.py | 26 +++++++++++++++++---------
 hiboo/user/views.py    | 13 +++++++++----
 2 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/hiboo/account/login.py b/hiboo/account/login.py
index 52b934d..ad5afca 100644
--- a/hiboo/account/login.py
+++ b/hiboo/account/login.py
@@ -86,21 +86,29 @@ def signup():
             return flask.redirect(utils.url_or_intent(".home"))
     return flask.render_template("account_signup.html", form=form)
 
-
-@blueprint.route("/reset/<token_uuid>", methods=["GET", "POST"])
-def reset(token_uuid):
-    token = models.ResetToken.query.get(token_uuid) or flask.abort(403)
-    if token.updated_at is not None or token.expired_at < datetime.datetime.now():
+@blueprint.route("/reset", methods=["GET", "POST"])
+def 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('.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"))
     form = forms.PasswordForm()
     del form.old
     if form.validate_on_submit():
-        token.expired_at = datetime.datetime.now()
-        models.db.session.add(token)
-        auth = token.user.auths[models.Auth.PASSWORD]
+        auth = user.auths[models.Auth.PASSWORD]
         auth.set_password(form.password.data)
-        models.log(models.History.PASSWORD, user=token.user)
+        models.log(models.History.PASSWORD, user=user)
         models.db.session.add(auth)
         models.db.session.commit()
         flask.flash(_("Successfully reset your password"), "success")
diff --git a/hiboo/user/views.py b/hiboo/user/views.py
index 5ea5874..86b4cdd 100644
--- a/hiboo/user/views.py
+++ b/hiboo/user/views.py
@@ -35,10 +35,15 @@ def details(user_uuid):
 def password_reset(user_uuid):
     user = models.User.query.get(user_uuid) or flask.abort(404)
     expired = datetime.datetime.now() + datetime.timedelta(days=1)
-    token = models.ResetToken(user=user, expired_at=expired)
-    models.db.session.add(token)
-    models.db.session.commit()
-    reset_link = flask.url_for("account.reset", token_uuid=token.uuid, _external=True)
+    payload = {
+        "exp": int(expired.timestamp()),
+        "aud": flask.url_for('account.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.reset", token=token, _external=True)
     flask.flash(_("Reset link: {}").format(reset_link), "success")
     return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
 
-- 
GitLab