Forgot Password Feature with Python and Flask

If you’re handling user authentication in your app, you’ve got two choices:

  1. Delegate it to an OAuth2 provider like Google or Facebook. These giants already have your users’ data, so why not let them do the heavy lifting?
  2. Take on the task of managing passwords yourself, because you’re either brave, naive, or work for a client who thinks OAuth2 is a type of shampoo.

If you’ve chosen the second option, congratulations! You now have to deal with password resets. This means giving users a way to securely reset their password when they inevitably forget it. Let’s discuss how this works under the hood.

Workflow

Here’s the high-level overview of how we’ll implement the “Forgot Password” feature:

  1. Create a JWT (JSON Web Token): This token will act as a temporary, signed credential to verify the user’s identity. It should have a short expiration time, because storing sensitive data in an email is like leaving your car unlocked in a bad neighborhood.
  2. Send the token via email: When the user requests a password reset, send them an email with the JWT embedded in a link.
  3. Reset password page: The email link will lead to a page where the user can enter a new password.
  4. Verify and update: The frontend submits the JWT and new password to the backend. If the JWT is valid, the backend updates the user’s password and everyone lives happily ever after (until the next bug report).
Forget Password Workflow

Now, let’s implement all this with Python and Flask.

Save New Password

First of all, let’s start by implementing the endpoint which accepts a new password from an authenticated request. To verify the received JWT, I use the library Flask-JWT-Extended.

@users_bp.route("/users/password/reset", methods=["POST"])
@jwt_required
def reset_password():
    doc = request.json

    new_password = doc["new_password"]

    current_user = get_jwt_identity()
    current_user.password = generate_password_hash(new_password)
    db.session.commit()

    return jsonify({}), 204

There is not much logic in there, I just validate the request and read the password from the request.

Let’s continue with an other endpoint, the one which sends the email with the JWT.

Send Authenticated Email

This endpoint, requires no authentication, as it will be used from an user who is unable to log in.

Inside, I first check the existence of the user with the given email address. Then, I create a JWT. And finally, I use the SMTP library to send an email.

@users_bp.route("/users/password/forgot", methods=["POST"])
def forgot_password():
    doc = request.json

    user = MyUser.query.filter(MyUser.email == doc[""]).one_or_none()
    if not user:
        raise Error("Unknown user")

    encoded_jwt = jwt.encode(
        {
            "sub": doc["email"],
            "exp": int(time.time()) + (60 * 60),
        },
        "my-secret-key",
        algorithm="HS256",
    )

    body = f"""
    To reset your password, click <a href="https://myfrontend.com?jwt={encoded_jwt}">here</a>.
    """

    try:
        msg = EmailMessage()
        msg.set_content(body)
        msg['Subject'] = "Reset your password"
        msg['From'] = "no-reply@enterprise.com"
        msg['To'] = doc["email"]

        # Connect to Gmail SMTP server
        with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
            smtp.login(from_email, app_password)  # Authenticate
            smtp.send_message(msg)  # Send the email

        print("Email sent successfully!")
    except Exception as e:
        print(f"Failed to send email: {e}")

    return jsonify({}), 204

Conclusion

The main goal of a “Forgot Password” feature is to securely authenticate the user through their email. By using a short-lived JWT, you minimize risks while ensuring the process remains user-friendly. Remember:

  • Keep JWTs short-lived to reduce the impact of exposure.
  • Use HTTPS for all communications (because plaintext HTTP is so 1990s).
  • Educate your users that “password” is a terrible password.

Implementing this feature might feel like extra work, but it’s essential for user trust.


Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading