0

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)
Random Nerd
  • 134
  • 1
  • 9
  • Downvotes are welcome if they come along with proper feedback for me to improve. Or else vaguely downvoting isn't either going to improve me or this awesome SO community as a whole. Thanks! – Random Nerd Oct 16 '18 at 07:26

1 Answers1

0

You're setting user.password, but the model does not have that attribute. password_hash is the field you need to set.

Also, it looks like you're trying to save the password itself; if that's the case, this is incorrect. You need to save the hash of the password, never the password itself. So that line should look more like this (hard to say if this is exactly correct, but it's the right idea).

user.password_hash = generate_password_hash(password) 
kungphu
  • 4,592
  • 3
  • 28
  • 37
  • I have set `self.password_hash = generate_password_hash(password)` in my *model.py*, so shouldn't it take care of generating hash for input password? – Random Nerd Oct 15 '18 at 06:11
  • It does when the user object is instantiated. You need to use that same process to update the hash when the password is changed. If you want to use `user.password = "..."` you could [create a property](https://docs.python.org/3.6/library/functions.html#property) called `User.password` that accepts the plain-text password and sets `self.password_hash` to the value of `generate_password_hash(password)`. – kungphu Oct 15 '18 at 06:14
  • Creating **property** is new for me so it seems am doing something wrong. I didn't alter my *password_reset* view and let it be as `user.password = form.new_password.data` but in my *models.py*, within `User` Class I added: `def password(value): self.password_hash = generate_password_hash(value) password_hash = property(password)` Could you please hint where I made a mistake. – Random Nerd Oct 15 '18 at 08:36