diff --git a/hiboo/account/login.py b/hiboo/account/login.py
index bc54424224193ffd3d88b5eb4bb53c31c41834cb..0b40e7d0e4b7ae31844aa94da800f2d066197919 100644
--- a/hiboo/account/login.py
+++ b/hiboo/account/login.py
@@ -2,6 +2,7 @@ from hiboo import models, utils, security
 from hiboo.account import blueprint, forms
 from flask_babel import lazy_gettext as _
 
+import datetime
 import flask_login
 import flask
 
@@ -49,3 +50,24 @@ def signup():
             flask_login.login_user(user)
             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():
+        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[0]
+        auth.set_password(form.password.data)
+        models.log(models.History.PASSWORD, user=token.user)
+        models.db.session.add(auth)
+        models.db.session.commit()
+        flask.flash(_("Successfully reset your password"), "success")
+        return flask.redirect(flask.url_for(".signin"))
+    return flask.render_template("account_reset.html", form=form)
\ No newline at end of file
diff --git a/hiboo/account/templates/account_reset.html b/hiboo/account/templates/account_reset.html
new file mode 100644
index 0000000000000000000000000000000000000000..04e69889d8b3478aa819be0b49a2c078b574645c
--- /dev/null
+++ b/hiboo/account/templates/account_reset.html
@@ -0,0 +1,3 @@
+{% extends "form.html" %}
+
+{% block title %}{% trans %}Reset your password{% endtrans %}{% endblock %}
diff --git a/hiboo/models.py b/hiboo/models.py
index 85127d356e0df943080b7e5e449769e2df58b8fe..d174f0ddda910bbb2c4765468c12fe328a8840d7 100644
--- a/hiboo/models.py
+++ b/hiboo/models.py
@@ -221,6 +221,15 @@ class ClaimName(db.Model):
     username = db.Column(db.String(255), nullable=False)
 
 
+class ResetToken(db.Model):
+    """ A reset token is used to reset authentication for a given user.
+    """
+    __tablename__ = "resettoken"
+
+    user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
+    user = db.relationship(User)
+    expired_at = db.Column(db.DateTime, nullable=False)
+
 
 class History(db.Model):
     """ Records an even in an account's or profile's lifetime.
diff --git a/hiboo/user/templates/user_details.html b/hiboo/user/templates/user_details.html
index 45d760b67034368c27c2df3ab2238f694007ccdd..dc159153b6eb8f5153e5c1c02283f30faab4b552 100644
--- a/hiboo/user/templates/user_details.html
+++ b/hiboo/user/templates/user_details.html
@@ -56,3 +56,7 @@
   </div>
 </div>
 {% endblock %}
+
+{% block actions %}
+<a href="{{ url_for(".password_reset", user_uuid=user.uuid) }}" class="btn btn-primary">{% trans %}Password reset{% endtrans %}</a>
+{% endblock %}
diff --git a/hiboo/user/users.py b/hiboo/user/users.py
index 7b45330a80e705ad07b10bfc02ba293fbad8787d..7a814435e2a8578f03e7f8f74fe18185a097691c 100644
--- a/hiboo/user/users.py
+++ b/hiboo/user/users.py
@@ -1,6 +1,8 @@
 from hiboo.user import blueprint, forms
 from hiboo import models, utils, security
+from flask_babel import lazy_gettext as _
 
+import datetime
 import flask
 
 
@@ -24,3 +26,17 @@ def list():
 def details(user_uuid):
     user = models.User.query.get(user_uuid) or flask.abort(404)
     return flask.render_template("user_details.html", user=user)
+
+
+@blueprint.route("/reset/<user_uuid>", methods=["GET", "POST"])
+@security.admin_required()
+@security.confirmation_required("generate a password reset link")
+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)
+    flask.flash(_("Reset link: {}").format(reset_link), "success")
+    return flask.redirect(flask.url_for(".details", user_uuid=user.uuid))
\ No newline at end of file
diff --git a/migrations/versions/665cdee2f311_add_password_reset.py b/migrations/versions/665cdee2f311_add_password_reset.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1bbc651550d3c27c16a8d2391eabd657ba3523e
--- /dev/null
+++ b/migrations/versions/665cdee2f311_add_password_reset.py
@@ -0,0 +1,32 @@
+""" Add reset tokens
+
+Revision ID: 665cdee2f311
+Revises: 29a70a960a97
+Create Date: 2020-08-02 14:34:56.262198
+"""
+
+from alembic import op
+import sqlalchemy as sa
+
+
+revision = '665cdee2f311'
+down_revision = '29a70a960a97'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    op.create_table('resettoken',
+    sa.Column('user_uuid', sa.String(length=36), nullable=True),
+    sa.Column('expired_at', sa.DateTime(), nullable=False),
+    sa.Column('uuid', sa.String(length=36), nullable=False),
+    sa.Column('created_at', sa.DateTime(), nullable=False),
+    sa.Column('updated_at', sa.DateTime(), nullable=True),
+    sa.Column('comment', sa.String(length=255), nullable=True),
+    sa.ForeignKeyConstraint(['user_uuid'], ['user.uuid'], ),
+    sa.PrimaryKeyConstraint('uuid')
+    )
+
+
+def downgrade():
+    op.drop_table('resettoken')