Flask randomly returns user as not logged in when they are

Question:

I have a reasonably sized Flask project that works fine when run locally, however, when run via Apache or Nginx Server (locally hosted or externally hosted) users will occasionally be prompted that they are not logged in before getting redirected several times.

Below is the log showing the user being redirected from / to /login 9 times before being logged in.

[10:57:35] "GET /user/1 HTTP/1.1" 200 12277 "redacted/fleet" 7.36"
[10:57:36] "GET /user/1 HTTP/1.1" 302 770 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2Fuser%2F1 HTTP/1.1" 302 736 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 787 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 745 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 789 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 747 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 789 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 749 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 793 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 751 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 793 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 751 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 794 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 752 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 795 "redacted/user/1" 7.36"
[10:57:36] "GET /login?next=%2F HTTP/1.1" 302 753 "redacted/user/1" 7.36"
[10:57:36] "GET / HTTP/1.1" 302 795 "redacted/user/1" 7.36"
[10:57:37] "GET /login?next=%2F HTTP/1.1" 302 752 "redacted/user/1" 7.36"
[10:57:37] "GET / HTTP/1.1" 302 797 "redacted/user/1" 7.36"
[10:57:37] "GET /login?next=%2F HTTP/1.1" 302 756 "redacted/user/1" 7.36"
[10:57:37] "GET /user/1 HTTP/1.1" 302 822 "redacted/user/1" 7.36"
[10:57:37] "GET /login?next=%2Fuser%2F1 HTTP/1.1" 302 756 "redacted/user/1" 7.36"
[10:57:37] "GET / HTTP/1.1" 200 4013 "redacted/user/1" 7.36"
[10:57:40] "GET /user/1 HTTP/1.1" 200 12273 "redacted/" 7.36"

The user does not need to log back in, and just refreshing the page a few times will work, however this issue keeps cropping up.

My user_loader function is as followed;

@login_manager.user_loader
def load_user(user_id):
    return db.session.get(User, int(user_id))

Because I cannot replicate the issue reliably I am unable to provide an example.

I am using;

  • Flask-SQLAlchemy 3.1.1
  • Flask 3.0.2
  • Flask-Login 0.6.3
  • Flask-WTF 1.2.1
  • Nginx version: nginx/1.18.0 (Ubuntu)

Database is mysql Ver 8.0.35 with PhpMyAdmin hosted on the same machine.

Nginx configurations;

server {
    listen 80;
    server_name [REDACTED URL];
        
    access_log      /var/log/nginx/access.[REDACTED URL].log;
    error_log       /var/log/nginx/error.[REDACTED URL].log;

    location / {
        proxy_pass http://unix:/home/gunicorn/run/[REDACTED URL].sock;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Prefix /;
    };
}

Service configuration;

[Unit]
Description=Development
After=network.target

[Service]
User=gunicorn
Group=www-data
WorkingDirectory=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]
Environment="PATH=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]/venv/bin"
ExecStart=/home/gunicorn/[REDACTED URL]/[REDACTED NAME]/venv/bin/gunicorn --workers 3 --bind unix:/home/gunicorn/run/[REDACTED URL].sock -m 007 wsgi:application

[Install]
WantedBy=multi-user.target

Application code sample;

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = '[REDACTED]'
app.config['permanent_session_lifetime'] = timedelta(days=364)

app.wsgi_app = ProxyFix(
    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)

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

mail = Mail(app)
db = SQLAlchemy(app)

@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        flash('Already logged in!', 'danger')
        return redirect(url_for('home'))

    email_login_form = EmailLoginForm()
    if 'email_login_submit' in request.form:
        if email_login_form.validate_on_submit():
            account_check = User.query.filter_by(user_email=email_login_form.user_email.data).first()
            if account_check is None:
                flash('Incorrect email or password!', 'danger')
                return redirect(url_for('login'))

            if not account_check.check_password(email_login_form.user_password.data):
                flash('Incorrect email or password!', 'danger')
                return redirect(url_for('login'))

            login_user(account_check, remember=True)

            return redirect(url_for('home'))

    return render_template('login.html', email_login_form=email_login_form)
        
@app.route("/", methods=['GET', 'POST'])
@login_required
def home():
    return render_template('dashboard.html')
        
if __name__ == "__main__":
    app.run(host='0.0.0.0')
Asked By: Reuben

||

Answers:

Managed to solve the issue. Turns out there was some development code left in;

with app.app_context():
    db.reflect()
    db.create_all()

Deleting this solved all the issues. Absolute nightmare to track down.

Answered By: Reuben