I am a beginner with Python and Flask (and even SO), so kindly pardon my below standard code & if my question is missing any required detail, please do keep me posted. Tried searching for an answer (read many tutorials like these) but unsuccessful. This was closest match on SO as per my understanding but didn't work for me.
My app has SQLite DB and Flask-Login for authentication. I am trying to reset password for registered user. So user clicks on 'Forgot Password' button on Login page (if user wasn't registered, he gets routed to Registration page), and that leads to another page where I ask for registered email id. Send an email to user with verification link, and once he/she clicks on that, route to Password reset page.
This Password reset page (associated view) is creating issue as per my understanding. Here, user enters new password but that doesn't get updated in my database. After reset, intended routing to Login page does happen with success message, but actually when I try to login with new password, it fails because it still authenticates with old password. Though there is a DateTime value as well, which I was simultaneously trying to feed in during password reset, and that entry was successful.
Hoping I conveyed my query okay enough. Here are 3 Views that I created for this password reset process:
# View for Password Reset form:
@app.route("/password_reset", methods=["GET","POST"])
def password_reset():
form = PasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
flash(u"Invalid/Unknown email address.")
return render_template("password_reset.html", form=form)
elif user is not None and form.new_password.data != form.new_pass_confirm.data:
flash(u"Password mismatch!")
return render_template("password_reset.html", form=form)
else:
user.passwordUpdated_on = datetime.now()
user.password = form.new_password.data #This is my problem line, I guess.
db.session.add(user)
db.session.commit()
flash("Password has been successfully updated!")
return redirect(url_for("login"))
return render_template("password_reset.html", form=form)
# Helper function to redirect User after clicking on password reset link:
@app.route("/reset/<token>")
def pwdreset_email(token):
try:
email = pwdreset_token(token)
except:
flash("Your password reset link is invalid or has expired.")
return redirect(url_for("support"))
return redirect(url_for("password_reset"))
# User Registration/Signup View:
@app.route("/forgot_password", methods=["GET","POST"])
def forgot_password():
form = ForgotPasswordForm()
if form.validate_on_submit():
# If User is registered with us:
user = User.query.filter_by(email=form.email.data).first()
if user is None:
flash(u"Unknown email address!")
return render_template("forgot_password.html", form=form)
# If User is registered and confirmed, sending Password Reset email:
if user.confirmed:
token = generate_pwdreset_token(user.email)
reset_url = url_for("pwdreset_email", token=token, _external=True)
html = render_template("password_email.html", confirm_url=reset_url)
subject = "Password Reset!"
send_email(user.email, subject, html)
db.session.add(user)
db.session.commit()
flash(u"Kindly check registered email for a password reset link!")
# Routing User to Login page:
return redirect(url_for("login"))
elif user.confirmed is False:
flash(u"Your email address must be confirmed before attempting a password reset.")
return redirect(url_for("unconfirmed"))
# Rendering a template for User to initiate Password Reset:
return render_template("forgot_password.html", form=form)
Here is my Model:
class User(db.Model, UserMixin):
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True, nullable=False)
username = db.Column(db.String(64), unique=True, nullable=False)
password_hash = db.Column(db.String(256), nullable=False)
passwordUpdated_on = db.Column(db.DateTime, nullable=True)
confirmed = db.Column(db.Boolean, nullable=False, default=False)
def __init__(self, email, username, password, passwordUpdated_on=None, confirmed=False):
self.email = email
self.username = username
self.password_hash = generate_password_hash(password) #Werkzeug
self.passwordUpdated_on = passwordUpdated_on
self.confirmed = confirmed
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Here is my configuration script:
class BaseConfig(object):
"""
Base configuration for Database and Mail settings.
"""
# Creating Database with preferred settings:
basedir = abspath(dirname(__file__))
SQLALCHEMY_DATABASE_URI = "sqlite:///" + join(basedir, "my_data.sqlite")
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECURITY_RECOVERABLE = True # Added this looking at other SO answer. Haven't yet read about it.
# Main Application configuration:
SECRET_KEY = "random_key"
SECURITY_PASSWORD_SALT = "random_password"
WTF_CSRF_ENABLED = True
DEBUG_TB_ENABLED = False
DEBUG_TB_INTERCEPT_REDIRECTS = False
Lastly my Forms:
class ForgotPasswordForm(FlaskForm):
email = StringField("Email Address: ", validators=[DataRequired()])
submit = SubmitField("Reset Password")
class PasswordResetForm(FlaskForm):
email = StringField("Email Address: ", validators=[DataRequired()])
new_password = PasswordField("New Password: ", validators=[DataRequired(), EqualTo("new_pass_confirm")])
new_pass_confirm = PasswordField("Confirm New Password: ", validators=[DataRequired()])
submit = SubmitField("Update Password")
And also my password_reset template below:
<form action="" method="POST">
{{ form.hidden_tag() }}
<div class="form-group">
<label for="email">Email Address: </label>
<input type="email" class="form-control form-control-sm" name="email" id="email" aria-describedby="emailHelp" value="">
</div>
<div class="form-group">
<label for="new_password"><h5 style="font-family:verdana; color: #514e0d"><b>New Password: </b></h5></label>
<input type="password" class="form-control form-control-sm" name="new_password" id="new_password" value="">
</div>
<div class="form-group">
<label for="new_pass_confirm">Confirm New Password: </label>
<input type="password" class="form-control form-control-sm" name="new_pass_confirm" id="new_pass_confirm" value="">
</div>
<div class="row">
<div class="col">
<a class="btn btn-warning btn-lg" href="{{ url_for("support") }}" role="button">Support </a>
</div>
<div class="col">
<button type="submit" class="btn btn-success btn-lg float-right">Update Password</button>
</div>
</div>
<br>
</form>
Any clue would be highly appreciated. Thanks for your time and once again, if I've missed providing any required information, please do let me know.
Solution: In my models.py, I added:
@property
def password(self):
"""
The password property will call werkzeug.security and
write the result to the 'password_hash' field.
Reading this property will return an error.
"""
raise AttributeError("password is not a readable attribute")
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)