2
votes

I have a login page that uses a custom FlaskForm (using WTForms). If a user enters the correct credentials, a PostgreSQL database is successfully queried (using flask-sqlalchemy) to see if a user with that name and (hashed) password exists. If there is such a user, login_user(user) is ran, and redirection to the homepage of my site is attempted.

I have implemented flask-login (as per the online documentation), but when a user provides valid credentials for logging in, they are redirected back to the login page (as if they had not provided valid credentials). I am using Google Chrome.

I have determined that after redirecting to the homepage, the current_user is of type AnonymousUserMixin (even though current user in the login function is of type User (which I have defined, inheriting all methods from UserMixin).

Here's what I have tried:

  • Ensured my code meets the specifications outlined in the Flask documentation

  • Browsed articles on StackOverflow, Reddit, and various blogs. From those, I have made the following changes to my code:

  • Inserted the hidden_tag() and csrf_token() fields into my login form (see the final code excerpt)

  • Added a secret key to my Flask application

  • Encoded and decoded (with utf8) the id of the current user (see below code, also in the User class definition further below)

    return str(self.id).encode('utf-8').decode('utf-8')

As per the flask-login documentation, I have put the following into my file application.py (the file where my flask code is):

At the top of the file:

login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

A user loader function:

@login_manager.user_loader
def load_user(id):
    id = db.execute("SELECT id FROM users WHERE id=:id", {"id": id})
    return User.get(current_user, id)

A user class (which inherits UserMixin):

class User(UserMixin):
    is_active = True
    is_anonymous = False
    def __init__(self, email, name, id, input_password_hash):
        self.id = id
        self.name = name
        self.email = email
        self.password_hash = input_password_hash

    def check_password(self, password, password_hash_byte_literal):
        return bcrypt.checkpw(password.encode('utf8'), password_hash_byte_literal)

    def get_id(self):
        return str(self.id).encode('utf-8').decode('utf-8')

    def get(self, user_id):
        id = db.execute("SELECT id FROM users WHERE id=:user_id", {"user_id": user_id})
        if id:
            name = db.execute("SELECT name FROM users WHERE id=:user_id", {"user_id": user_id})
            email = db.execute("SELECT email FROM users WHERE id=:user_id", {"user_id": user_id})
            password_hash = db.execute("SELECT password_hash FROM users WHERE id=:user_id", {"user_id": user_id})
            user_name_string = ''
            user_email_string = ''
            user_password_hash_string = ''
            for row in name:
                for i in range(len(row)):
                    user_name_string += row[i]
            for row in email:
                for i in range(len(row)):
                    user_email_string += row[i]
            for row in password_hash:
                for i in range(len(row)):
                    user_password_hash_string += row[i]
            return User(user_email_string, user_name_string, user_id, user_password_hash_string)
        else:
            return None

Below is my login route:

@app.route("/login", methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        email = form.email.data
        password = form.password.data
        user_pw_hash = (db.execute("SELECT password_hash FROM users WHERE email=:email", {"email": email}).fetchone())
        user_id = (db.execute("SELECT id FROM users WHERE email=:email", {"email": email}).fetchone())
        if user_id:
            password_hash_string = ''
            id_string = str(user_id)
            for row in user_pw_hash:
                for i in range(len(row)):
                    password_hash_string += row[i]
            user_id_int = int(id_string[1])
            user = User.get(user, user_id_int)
            password_hash_byte_literal = bytes(password_hash_string, encoding='utf8')
            correct_password = User.check_password(user, password, password_hash_byte_literal)
            if correct_password:
                login_user(user)
                next = url_for("index")
                if not is_safe_url(next, {"http://127.0.0.1:5000"}):
                    return abort(400)
                return redirect(next or url_for("login"))
            else:
                return render_template("login.html", message="Incorrect username or password.", form=form)
        else:
            return render_template("login.html", message="No account with that email address was found.", form=form)

    else:
        return render_template("login.html", form=form)

As per the flask-login documentation, I login the user with the login_user function (see above), and I check to see if the next url (my homepage -- "index") is safe. If it is, I proceed to redirect the user to that page.

Also, below is my login form (which includes the hidden_tag() and csrf_token() fields).

<form method="post" action="/login">
    {{ form.hidden_tag() }}
    {{ form.csrf_token() }}
    {{ wtf.form_field(form.email) }}
    {{ wtf.form_field(form.password) }}
    <button type="submit" value="submit">Submit</button><br>
</form>

I realize that this code does not yet properly sanitize inputs before executing PostgreSQL commands. I will work on fixing this issue very soon.

Imports:

import os
from flask import flash, Flask, session, redirect, render_template, request, url_for
from flask_bootstrap import Bootstrap
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from flask_session import Session
from is_safe_url import is_safe_url
from sqlalchemy import create_engine
from sqlalchemy.orm import scoped_session, sessionmaker
from forms import LoginForm, RegistrationForm, ReviewForm   # Custom WTForms I wrote
import bcrypt

Command Line Output when User Submits Form and Redirected to Homepage is Attempted (index)

127.0.0.1 - - [15/Jun/2020 18:42:35] "GET /login HTTP/1.1" 200 -

127.0.0.1 - - [15/Jun/2020 18:42:48] "POST /login HTTP/1.1" 302 -

127.0.0.1 - - [15/Jun/2020 18:42:48] "GET / HTTP/1.1" 302 -

127.0.0.1 - - [15/Jun/2020 18:42:48] "GET /login?next=%2F HTTP/1.1" 200 -

I am using Visual Studio code (and its PowerShell) to run and edit this Flask application.

Versions:

Windows 10
Google Chrome Version 83.0.4103.106 (Official Build) (64-bit)
bcrypt 3.1.7
email-validator 1.1.1
Python 3.8.2
Flask 1.1.2
Flask-WTF 0.14.3
Flask-SQLAlchemy 2.4.3
Flask-Session 0.3.2
Flask-Login 0.5.0
Flask-Bootstrap
WTForms 2.3.1
SQLAlchemy 1.3.16
mysql-connector-python 8.0.19
mysql-client 0.0.1
Jinja2 2.11.2
itsdangerous 1.1.0
is-safe-url 1.0

Thank you in advance for your help!

Update

Below is my updated code (with changes made based on others' insightful comments):

Login Function:

@app.route("/login", methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        email = form.email.data
        password = form.password.data
        user_id = (db.execute("SELECT id FROM users WHERE email=:email", {"email": email}).fetchone())
        if user_id:
            user_pw_hash = (db.execute("SELECT password_hash FROM users WHERE email=:email", {"email": email}).fetchone())
            password_hash_string = user_pw_hash.password_hash
            user = User(None, None, None, False)
            user_id_int = user_id.id
            user = load_user(user_id_int)
            password_hash_byte_literal = bytes(password_hash_string, encoding='utf8')
            correct_password = User.check_password(user, password, password_hash_byte_literal)
            if correct_password:
                login_user(user)
                next = url_for("index")
                if not is_safe_url(next, {"http://127.0.0.1:5000"}):
                    return abort(400)
                else:
                    return redirect(next or url_for("login"))
            else:
                return render_template("login.html", message="Incorrect email or password.", form=form)
        else:
            return render_template("login.html", message="No account with that email address was found.", form=form)
    else:
        return render_template("login.html", form=form)

Login Manager User Loader:

@login_manager.user_loader
def load_user(id):
    user_data = db.execute("SELECT * FROM users WHERE id=:id", {"id": id}).fetchone()
    if user_data:
        return User(user_data.email, user_data.name, id, user_data.password_hash)
    else:
        return None

A Get ID function from my User class:

    def get_id(self):
        return self.id

The above two function work correctly, but users are still redirected to the login page after attempting to sign in with valid credentials.

Again, thank you all for your help; it is greatly appreciated.

2
With problems like these I find that it’s easiest to write a test that is failing, hook a debugger up to it and pick at it. Not to say you aren’t doing this, but the debugger is key.Daniel Butler
Daniel, thank you for the suggestion. How do you recommend testing the software (i.e. what packages, software, etc. should I use)?user13736908
I personally use Pycharm and its debugger. I would run the flask site in DEBUG mode. Make the request in the actual site with dev tools open, see what is actually being sent. Create a test using flasks test client (google it, it’s about 5 lines of code) sending that same data. Then using that start making changes rerunning the test each time. Hope that makes sense.Daniel Butler
Daniel, thank you for the suggestion. I’ll work on doing what you said and will let you know what I find.user13736908
Please don't vandalize your posts. By posting on the Stack Exchange network, you've granted a non-revocable right, under the CC BY-SA 4.0 license, for Stack Exchange to distribute that content (i.e. regardless of your future choices). By Stack Exchange policy, the non-vandalized version of the post is the one which is distributed, and thus, any vandalism will be reverted. If you want to know more about deleting a post please see: How does deleting work?.cigien

2 Answers

0
votes

I'm having the same problem as you, when I don't set the remember=True, I can not redirect after using flask.login_user

Per flask-login docs: https://flask-login.readthedocs.io/en/latest/#flask_login.login_user remember (bool) – Whether to remember the user after their session expires. Defaults to False.

so I think I have some configuration that my session expires right after my initial request, because if I set remember to True

so instead of doing:

login_user(user)

try

login_user(user=user, remember=True)

Suggestion 2:

my guess would be to take a double look into your function to get the user from the database def get(self, user_id):, make sure this is returning the user object properly and not None. Also the login_user() from flask.login should return True if the login is successful. This how I would find and get the user from the db using id:

def find_user_by_id(user_id: int):
row = db.execute('select * from users u where u.id =:user_id',
                 {'user_id': user_id}).fetchone()
if row is None:
    return None
else:
    return User(user_id=row.id, first_name=row.first_name,
                last_name=row.last_name, email=row.email, reviews=None, password_hash=row.password)
0
votes

You're not implementing the flask-login requirements correctly. Try using the default user_loader callback and seeing if it fixes your issue.

@login_manager.user_loader
def load_user(id):
    # Whichever method you use to load a user, it needs to be guaranteed unique
    field_values = list(db.execute("SELECT id, name, email, password_hash FROM users WHERE id=:id", {"id": id}))
    
    return User(**dict(field_values))

and in your User model

def get_id(self):
    # this matches what user_loader needs to uniquely load a user
    return self.id

See: https://flask-login.readthedocs.io/en/latest/#how-it-works