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.
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