Context: I am currently using Flask & Python for a website that I am trying to create. I am also using MongoDB for my database. So far, I have used various flask libraries, but I seem to be having trouble with user authentication, specifically, LoginManager(). I believe that this could simply be due to bad configuration or something I am missing. I should state that I am no expert. I only took a class this semester in school, and I am simply trying to reproduce what I learnt. I sincerely apologize for the long message, but I wanted to give as much context as possible
Problem: I seem to be having an issue with my login() logic. Once the website renders, and I implement a new user registration, this works. Now once I try to login that user, the user should get logged in and redirected to the user's account page, but this doesnt happen. I get a flash warning on the login page saying that I should log in to access this page. Here is a screenshot: Error description
Now given that I had no such message in my code, I did some research and found out that this was the default message flask gives when a user that is not authenticated tries to access a specific resource. Given that for a user to be able to access their account, they must be logged in, so @login_required is a decorator for my account route. So from what I understood, it seems the user wasnt properly being authenticated. I tried to print status of current_user in my account route and login route. Below are the logs from the print statement in the login route, but for account, but since this doesnt even render,I cannot see the status at that point. So I tried to approach the problem from this perspective
Problem-solving approach: I tried first by adding print statements around where the user gets authenticated and logged in, within the login() route. This was the resulting logs in my terminal:
From what I understand, the user is actually getting logged in and authenticated. The redirection tries to occur, but doesnt, which leads me to believe that I might be missing something in my configuration or the logic of inheritance used in model.py messes up MongoDB.
Configuration files: I have an init.py where I create LoginManager() object. In another file called model.py, I set my load_user(). In config,py, which I will not be posting here, I do have a secret key initialized, using os.secrets from python. And finally, my routes.py, which contains all the routes.
init.py
#3rd party packages
from flask import Flask, render_template, request, redirect, url_for
from flask_mongoengine import MongoEngine
from flask_bcrypt import Bcrypt
from flask_login import LoginManager, current_user
# local -- created objects
from .client import SchoolClient
#--------------- CONFIGURING IMPORTANT FLASK EXTENSIONS ------------------------
db = MongoEngine() # Creating database object to interact with MongoDB
login_manager = LoginManager() # manages user's sessions,settings for logging in
bcrypt = Bcrypt() # for password hashing.
school_client = SchoolClient()
# ------------------ importing blueprints to be used --------------
from .content.routes import content
from .users.routes import users
# custom 404
def custom_404(e):
return render_template("404.html"), 404
# Todo
def create_app(test_config=None):
app = Flask(__name__)
# storing the configuration of out Flask app.
app.config.from_pyfile("config.py", silent=False)
# this simply checks if I set up configuration for testing
if test_config is not None:
app.config.update(test_config)
# initializes the created app based on what is needed
db.init_app(app)
login_manager.init_app(app)
bcrypt.init_app(app)
# registering the blueprint to the created app and creating a custom error page
app.register_blueprint(users)
app.register_blueprint(content)
app.register_error_handler(404, custom_404)
login_manager.login_view = "users.login"
return app
model.py
from flask_login import UserMixin # good for user authentication
from . import db, login_manager
from .utils import curr_datetime
# ------------------------------USER MANAGEMENT---------------------------------
# This function, decorated with @login_manager.user_loader, is used to load a user from the database. It uses the argument, educator_id to find the specific educator that might exist in the database, and returns the first occurrence of the educator with such username.
@login_manager.user_loader
def load_user(user_id):
return User.objects(username = user_id).first()
class User(db.Document, UserMixin):
# necessary, so MongoEngine can tell this class will be inherited. Found on stack overflow. This seems to be set to False by default
meta = {'allow_inheritance': True}
# Common field for all subclasses to be inherited.
firstname = db.StringField(required=True, min_length=2, max_length=40)
lastname = db.StringField(required=True, min_length=2, max_length=40)
username = db.StringField(required=True, unique=True, min_length=2, max_length=40)
email = db.EmailField(required=True, unique=True)
password = db.StringField(required=True, min_length=12) # length of 12
profile_pic = db.ImageField()
bio = db.StringField()
# returns the user's first name and last name using current states
def fullname(self):
return f"{self.firstname} {self.lastname}"
# helps user to update bio in real time
def set_bio(self, bio_msg):
self.bio = bio_msg
self.save() # saves to databse??
def set_profile_pic(self, profile_pic):
self.profile_pic = profile_pic
self.save()
# This class describes the user model and what each user (Educator's) state and characteristics should have
class Educator(User):
institution = db.StringField(required=True)
role = db.StringField("Educator")
# Get the corresponding place that they teach
def get_institution(self):
return self.institution
def get_role(self):
return self.role
# represents student class
class Student(User):
college = db.StringField(required=True)
role = db.StringField("Student")
def get_institution(self):
return self.college
def get_role(self):
return self.role
routes.py
# flask extensions and extra libraries
from flask import Blueprint, redirect, url_for, render_template, request, flash
from flask_login import current_user, login_required, login_user, logout_user
from .. import bcrypt
from io import BytesIO
from base64 import b64encode
from werkzeug.utils import secure_filename
from base64 import b64encode
# # local imports
from ..models import User, Student, InformalEducator, Educator
from ..forms import RegistrationForm, LoginForm, UpdateUsernameForm, UpdateProfilePicForm
users = Blueprint("users", __name__)
# ======================== USER MANAGEMENT VIEWS ==========================
# This function helps users to register for a new account on the website. This could either be a Student, Teacher or informal educator, as stated in models.py
@users.route("/register", methods=["GET", "POST"])
def register():
if current_user.is_authenticated:
return redirect(url_for('content.index')) #TODO
# creates an instance of the form that is needed to register
form = RegistrationForm()
if request.method == 'POST':
if form.validate_on_submit():
# hashes the user's password after user types it in
hashed_pwd = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
# user = User(firstname=form.firstname.data, lastname=form.lastname.data, username=form.username.data, email=form.email.data, password=hashed_pwd)
# Deciding what instance of user to create based on what user
# indicates what role is
if form.role.data == 'Educator':
user = Educator(
firstname=form.firstname.data,
lastname=form.lastname.data,
username=form.username.data,
email=form.email.data,
password=hashed_pwd,
institution=form.institution.data
)
elif form.role.data == 'Student':
user = Student(
firstname=form.firstname.data,
lastname=form.lastname.data,
username=form.username.data,
email=form.email.data,
password=hashed_pwd,
institution=form.institution.data
)
else:
user = InformalEducator(
firstname=form.firstname.data,
lastname=form.lastname.data,
username=form.username.data,
email=form.email.data,
password=hashed_pwd,
institution=form.institution.data
)
user.save()
flash('Your account has been created! You are now able to log in', 'success')
# after logging the user in, we redirect them to login
return redirect(url_for('users.login'))
return render_template('register.html', title = 'Register', form=form)
@users.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
# redirect authenticated users
return redirect(url_for('content.index'))
form = LoginForm()
if request.method == 'POST':
if form.validate_on_submit():
# Check if user exists and the password is correct
user = User.objects(username = form.username.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
print("This is user before authenication:", current_user)
login_user(user)
print("This is user after authenication:", current_user)
return redirect(url_for('users.account'))
else:
# If user is not authenticated, flash a message
flash("Login Failed. Account might not exist or password might be wrong!")
return render_template('login.html', title='Login', form=form)
@users.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.', 'info')
return redirect(url_for('content.index'))
@users.route("/account", methods=["GET", "POST"])
@login_required
def account():
propic_update = UpdateProfilePicForm()
username_update = UpdateUsernameForm()
propic = None
if request.method == "POST":
# username update
if username_update.validate():
current_user.modify(username = username_update.username.data)
current_user.save()
flash('Your username has been updated.', 'success')
return redirect(url_for('users.account'))
# profile picture update
if propic_update.validate():
image = propic_update.picture.data
filename = secure_filename(image.filename)
content_type = f'images/{filename[-3:]}'
if current_user.profile_pic.get() is None:
# user doesn't have a profile picture => add one
current_user.profile_pic.put(image.stream, content_type=content_type)
else:
# user has a profile picture => replace it
current_user.profile_pic.replace(image.stream, content_type=content_type)
current_user.save()
flash('Your profile picture has been updated.', 'success')
return redirect(url_for('users.account'))
# get's the current user's profile picture if it exists
if current_user.profile_pic:
propic_bytes = BytesIO(current_user.profile_pic.read())
propic = b64encode(propic_bytes.getvalue()).decode()
else:
propic = None
return render_template(
'account.html',
image = propic,
update_username_form = username_update,
update_profile_picture_form=propic_update
)
Your descriptions of the application are very helpful in narrowing down the problem. You are correct in your assumption that the user is logged in successfully.
I think the problem arises in the load_user()
function. This is used for every request to find the user and save it in the variable current_user
. If the variable is not set, the user is considered not logged in. However, no applicable user is found in the function and the user is therefore considered not logged in after a redirect.
You are trying to query the user by username. I think this is the wrong property here. Try comparing with the primary key that is assigned to each object when it is added to the database.
To find objects in the database, an additional, unique column id
is automatically assigned, which can also be accessed using the alias pk
. User login is based on this identifier.
@login_manager.user_loader
def load_user(user_id):
return User.objects(pk=user_id).first()