Expected behavior:
- User logs in and is redirected to the home page where user info is loaded from API and persisted
Error:
- Getting 401 from API on the home page
I have created a relatively large app with Flask and I have used Flask-login and SQLAlchemy with MySQL. My database is on Avian, my backend is hosted on Heroku, and the frontend which is a react application is hosted on Vercel.
While I was testing my system in development there were no issues, however, after deploying to Heroku (I also tried with PythonAnywhere) there was a single issue. After logging in the user I get a status 200 successfully logged in, but after the user is redirected to the home page I get a 401 error from my API saying the user is not logged in.
What I have tried so far:
- Using session type SQLAlchemy and connecting to my database to persist the session state
- Adding session cookie domain
- Using Flask-session
- Adding Flask-tailsman to enforce HTTPS
- Checking CORS settings and making sure to include all domains
- Ensuring the user_loader is in order
- Using
web: gunicorn app:app --preloadin the Procfile
Additional specs: Python 3.8, Flask 3.0.2
Heroku logs:
2024-03-15T05:51:18.006552+00:00 app[web.1]: 10.1.26.237 - - [15/Mar/2024:05:51:17 +0000] "OPTIONS /api/users/login HTTP/1.1" 200 0 "<my url>/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
2024-03-15T05:51:18.006707+00:00 heroku[router]: at=info method=OPTIONS path="/api/users/login" host=<heroku project>.herokuapp.com request_id=575d6668-8725-428c-b338-8fc83134e4bf fwd="60.254.90.89" dyno=web.1 connect=0ms service=2ms status=200 bytes=679 protocol=https
2024-03-15T05:51:19.955045+00:00 app[web.1]: 10.1.26.237 - - [15/Mar/2024:05:51:19 +0000] "POST /api/users/login HTTP/1.1" 200 38 "<my url>/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
2024-03-15T05:51:19.955217+00:00 heroku[router]: at=info method=POST path="/api/users/login" host=<heroku project>.herokuapp.com request_id=85ae383d-020b-40b5-94fd-968652d892d4 fwd="60.254.90.89" dyno=web.1 connect=0ms service=1627ms status=200 bytes=961 protocol=https
2024-03-15T05:51:21.549672+00:00 app[web.1]: 10.1.26.237 - - [15/Mar/2024:05:51:21 +0000] "OPTIONS /api/users/status HTTP/1.1" 200 0 "<my url>/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
2024-03-15T05:51:21.549804+00:00 heroku[router]: at=info method=OPTIONS path="/api/users/status" host=<heroku project>.herokuapp.com request_id=722dfc4b-e1df-4796-90c0-55ec73788ee3 fwd="60.254.90.89" dyno=web.1 connect=0ms service=2ms status=200 bytes=673 protocol=https
2024-03-15T05:51:21.871561+00:00 heroku[router]: at=info method=GET path="/api/users/status" host=<heroku>.herokuapp.com request_id=5e32fc4b-3011-4982-ae4d-bc4baf91b7b9 fwd="60.254.90.89" dyno=web.1 connect=0ms service=2ms status=401 bytes=569 protocol=https
Code (Minified):
- create_app.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from flask_session import Session
import os
from flask_talisman import Talisman
# Sets up SQLAlchemy database
db = SQLAlchemy()
# Creates flask app
def create_app():
app = Flask(
__name__,
static_url_path="",
static_folder="../client/build",
template_folder="build",
) # Initializes flask app
CORS(app, origins=["<localhost>", "<my url>", "<my url with www>", "<vercel>", "<vercel1>", "<vercel2>"])
app.config[
"SQLALCHEMY_DATABASE_URI"
] = "mysql+pymysql://<avn database url>"
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") # Secret key for flask app
app.config["UPLOAD_FOLDER"] = "static/images"
app.config['CORS_LOGGING'] = True
db.init_app(app)
app.config["SESSION_TYPE"] = "sqlalchemy"
app.config["SESSION_SQLALCHEMY"] = db # the SQLAlchemy object you have already created
Session(app)
app.config['SESSION_COOKIE_DOMAIN'] = '.<my url>'
return app
# Initializes flask app for use in other files
app = create_app()
Talisman(app)
app.py
from flask_login import LoginManager
from server_module.create_app import app
from server_module.models import User
# Sets up login manager
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = "loginUser"
# User loader
@login_manager.user_loader
def load_user(id):
user = User.query.get(id)
return user
# Handles unauthorized access
@login_manager.unauthorized_handler
def unauthorized_handler():
return "Unauthorized", 401
# Loads user
login_manager.user_loader(load_user)
models.py
import uuid
from sqlalchemy.sql import func
from werkzeug.security import generate_password_hash
from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin
from sqlalchemy_serializer import SerializerMixin
from ..create_app import db
class User(db.Model, UserMixin, SerializerMixin):
# These define columns of the User table
id = db.Column(
db.String(40),
primary_key=True,
nullable=False,
default=lambda: str(uuid.uuid4()),
)
email = db.Column(db.String(150), nullable=False, unique=True)
password = db.Column(db.String(500), nullable=False)
def __init__(self, email, password):
self.email = email
self.password = generate_password_hash(password=password)
users.py
from flask import Blueprint, request
from flask_login import login_user, current_user
from ..models import User
from werkzeug.security import check_password_hash
from ..utility import error_msg
users_blueprint = Blueprint("users", __name__)
# The login API
@users_blueprint.route("/login", methods=["POST"]) # Completed
def loginUser():
try:
try:
data = request.get_json()
except:
return error_msg()
if not all(key in data for key in ("password", "email")):
return error_msg()
user = User.query.filter_by(email=data["email"]).first()
if not user:
return error_msg(404)
if not check_password_hash(user.password, data["password"]):
return {"msg": "Incorrect password"}, 400
login_user(user, remember=True) # Logs user in
return {"msg": "User logged in successfully"}, 200
except Exception as e:
print("An error occurred: ", e)
return error_msg(500, e)
# User status API, gets user status for the nav bar
@users_blueprint.route("/status", methods=["GET"]) # Completed
def getUserStatus():
try:
if current_user.is_authenticated:
obj = current_user.to_dict(
only=(
"username",
"type",
"status",
"avatar",
"id"
)
)
return {"msg": obj}, 200
else:
return error_msg(401)
except Exception as e:
print("An error occurred: ", e)
return error_msg(500, e)