diff --git a/app/auth/forms.py b/app/auth/forms.py
index 582e6a176..c1bf9b7b2 100644
--- a/app/auth/forms.py
+++ b/app/auth/forms.py
@@ -42,3 +42,16 @@ class ChangePasswordForm(FlaskForm):
     password2 = PasswordField('Confirm new password',
                               validators=[DataRequired()])
     submit = SubmitField('Update Password')
+
+
+class PasswordResetRequestForm(FlaskForm):
+    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
+                                             Email()])
+    submit = SubmitField('Reset Password')
+
+
+class PasswordResetForm(FlaskForm):
+    password = PasswordField('New Password', validators=[
+        DataRequired(), EqualTo('password2', message='Passwords must match')])
+    password2 = PasswordField('Confirm password', validators=[DataRequired()])
+    submit = SubmitField('Reset Password')
diff --git a/app/auth/views.py b/app/auth/views.py
index 2f9cfd06c..be0db842d 100644
--- a/app/auth/views.py
+++ b/app/auth/views.py
@@ -5,7 +5,8 @@
 from .. import db
 from ..models import User
 from ..email import send_email
-from .forms import LoginForm, RegistrationForm, ChangePasswordForm
+from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
+    PasswordResetRequestForm, PasswordResetForm
 
 
 @auth.before_app_request
@@ -102,3 +103,36 @@ def change_password():
         else:
             flash('Invalid password.')
     return render_template("auth/change_password.html", form=form)
+
+
+@auth.route('/reset', methods=['GET', 'POST'])
+def password_reset_request():
+    if not current_user.is_anonymous:
+        return redirect(url_for('main.index'))
+    form = PasswordResetRequestForm()
+    if form.validate_on_submit():
+        user = User.query.filter_by(email=form.email.data.lower()).first()
+        if user:
+            token = user.generate_reset_token()
+            send_email(user.email, 'Reset Your Password',
+                       'auth/email/reset_password',
+                       user=user, token=token)
+        flash('An email with instructions to reset your password has been '
+              'sent to you.')
+        return redirect(url_for('auth.login'))
+    return render_template('auth/reset_password.html', form=form)
+
+
+@auth.route('/reset/<token>', methods=['GET', 'POST'])
+def password_reset(token):
+    if not current_user.is_anonymous:
+        return redirect(url_for('main.index'))
+    form = PasswordResetForm()
+    if form.validate_on_submit():
+        if User.reset_password(token, form.password.data):
+            db.session.commit()
+            flash('Your password has been updated.')
+            return redirect(url_for('auth.login'))
+        else:
+            return redirect(url_for('main.index'))
+    return render_template('auth/reset_password.html', form=form)
diff --git a/app/models.py b/app/models.py
index 6c6030e83..0584cf9ca 100644
--- a/app/models.py
+++ b/app/models.py
@@ -51,6 +51,24 @@ def confirm(self, token):
         db.session.add(self)
         return True
 
+    def generate_reset_token(self, expiration=3600):
+        s = Serializer(current_app.config['SECRET_KEY'], expiration)
+        return s.dumps({'reset': self.id}).decode('utf-8')
+
+    @staticmethod
+    def reset_password(token, new_password):
+        s = Serializer(current_app.config['SECRET_KEY'])
+        try:
+            data = s.loads(token.encode('utf-8'))
+        except:
+            return False
+        user = User.query.get(data.get('reset'))
+        if user is None:
+            return False
+        user.password = new_password
+        db.session.add(user)
+        return True
+
     def __repr__(self):
         return '<User %r>' % self.username
 
diff --git a/app/templates/auth/email/reset_password.html b/app/templates/auth/email/reset_password.html
new file mode 100644
index 000000000..1eafdfe16
--- /dev/null
+++ b/app/templates/auth/email/reset_password.html
@@ -0,0 +1,8 @@
+<p>Dear {{ user.username }},</p>
+<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
+<p>Alternatively, you can paste the following link in your browser's address bar:</p>
+<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
+<p>If you have not requested a password reset simply ignore this message.</p>
+<p>Sincerely,</p>
+<p>The Flasky Team</p>
+<p><small>Note: replies to this email address are not monitored.</small></p>
diff --git a/app/templates/auth/email/reset_password.txt b/app/templates/auth/email/reset_password.txt
new file mode 100644
index 000000000..fc6826c07
--- /dev/null
+++ b/app/templates/auth/email/reset_password.txt
@@ -0,0 +1,13 @@
+Dear {{ user.username }},
+
+To reset your password click on the following link:
+
+{{ url_for('auth.password_reset', token=token, _external=True) }}
+
+If you have not requested a password reset simply ignore this message.
+
+Sincerely,
+
+The Flasky Team
+
+Note: replies to this email address are not monitored.
diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html
index 1e14c7f5a..136a7539b 100644
--- a/app/templates/auth/login.html
+++ b/app/templates/auth/login.html
@@ -10,6 +10,7 @@ <h1>Login</h1>
 <div class="col-md-4">
     {{ wtf.quick_form(form) }}
     <br>
+    <p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
     <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
 </div>
 {% endblock %}
diff --git a/app/templates/auth/reset_password.html b/app/templates/auth/reset_password.html
new file mode 100644
index 000000000..995007744
--- /dev/null
+++ b/app/templates/auth/reset_password.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}Flasky - Password Reset{% endblock %}
+
+{% block page_content %}
+<div class="page-header">
+    <h1>Reset Your Password</h1>
+</div>
+<div class="col-md-4">
+    {{ wtf.quick_form(form) }}
+</div>
+{% endblock %}
\ No newline at end of file
diff --git a/tests/test_user_model.py b/tests/test_user_model.py
index 4c8765774..8436e49b8 100644
--- a/tests/test_user_model.py
+++ b/tests/test_user_model.py
@@ -58,3 +58,19 @@ def test_expired_confirmation_token(self):
         token = u.generate_confirmation_token(1)
         time.sleep(2)
         self.assertFalse(u.confirm(token))
+
+    def test_valid_reset_token(self):
+        u = User(password='cat')
+        db.session.add(u)
+        db.session.commit()
+        token = u.generate_reset_token()
+        self.assertTrue(User.reset_password(token, 'dog'))
+        self.assertTrue(u.verify_password('dog'))
+
+    def test_invalid_reset_token(self):
+        u = User(password='cat')
+        db.session.add(u)
+        db.session.commit()
+        token = u.generate_reset_token()
+        self.assertFalse(User.reset_password(token + 'a', 'horse'))
+        self.assertTrue(u.verify_password('cat'))