mrkeyoor.com_
Sat 08 Aug 21:01 UTC
PyPIWeb Backendupdated 08 Aug 2026

flask-login

Session management for Flask, and only that. You give it a LoginManager, a user_loader callback that turns a stored id back into your user object, and user objects with four attributes it can read. In return it keeps the logged-in user's id in the Flask session, exposes that user as current_user in views and templates, guards views with a login_required decorator, and optionally issues a signed remember-me cookie. It has no opinion about your database, no password hashing, no registration, no roles and no tokens.

Verdict

Still the standard way to hold a login session in a server-rendered Flask app, and its narrow scope is a feature when your user storage is unusual. Choose it knowing the release before this one was in 2022, that everything around the session (passwords, registration, redirect validation) is code you write, and that the security defaults on the remember-me cookie need tightening by hand.

API stability5/5LoginManager, user_loader, login_user, logout_user, current_user and login_required have not changed shape since 0.6.0 in March 2022, and the deprecations flagged then (header_loader, setup_app) are the only moving parts. Nothing is likely to break, partly because nothing is being released. Code written against it years ago still runs unchanged.
Docs4/5The Read the Docs site walks through the user object contract, the loader callbacks, remember-me, session protection, fresh logins and the test client, and every configuration key is listed with its default. It is honest about scope, saying up front that it does not handle registration or password recovery. The gap is that it is a single long page with few complete worked examples, so real integrations still involve reading the source.
Maintenance1/5The last release is 0.6.3 from 2023-10-30, which existed only to restore compatibility with Flask 3 and Werkzeug 3, and the last push to the repository was 2025-08-27. Nineteen issues and pull requests are open across 25 total releases. It is not archived and it still works, but a package that ships one compatibility fix every couple of years cannot be relied on to answer the next framework break quickly.
Ecosystem4/5Roughly 6,511,738 weekly downloads and 3,678 stars, and it is the assumed default in almost every Flask tutorial and in extensions such as Flask-Admin and Flask-Principal that read current_user directly. The size of that installed base is the main reason to expect it to keep working, since the surrounding ecosystem would notice a break immediately.

Use it if

  • You have a server-rendered Flask app with cookie sessions and want the login state plumbing without adopting a whole auth framework
  • Your user storage is unusual (LDAP, an internal service, a document store) and you want to keep total control of how a user is loaded from an id
  • You need step-up authentication, which login_fresh, fresh_login_required and confirm_login give you for sensitive views without a second session system
  • You want the small surface deliberately: four attributes on a user object, one loader callback, one decorator, and current_user available in Jinja templates
Skip it if

Setup reality

Install is one package and two real dependencies (Flask and Werkzeug), but nothing works until app.secret_key is set, because the whole design stores the user id in Flask's signed session cookie. The user_loader callback receives a string, always, since the id round-trips through the session as text, so a database lookup with an integer primary key needs an int() conversion or every lookup silently misses. That callback must return None for an unknown id rather than raising, otherwise a stale session cookie turns into a 500 on every request. UserMixin supplies is_authenticated, is_active, is_anonymous and get_id, and get_id must return something stable; using an email that users can change logs everyone out of that account. Remember-me is off by default and its defaults are worth reading: a 365 day cookie, HttpOnly on, Secure off, and SameSite unset, so REMEMBER_COOKIE_SECURE and REMEMBER_COOKIE_SAMESITE are your job in production. session_protection defaults to 'basic', which marks a session non-fresh when the client identifier changes; setting it to 'strong' logs the user out instead, and that will sign out mobile users on flaky networks. login_view must be set or an unauthenticated request raises 401 instead of redirecting. The redirect target lands in request.args['next'] and is not validated, so check it against your own host before redirecting. For tests, FlaskLoginClient lets you set app.test_client_class and pass user= to skip the login flow.

Patterns

Wire the extension into an appsetup-login-manager

from flask import Flask
from flask_login import LoginManager

app = Flask(__name__)
app.secret_key = os.environ['SECRET_KEY']

login_manager = LoginManager()
login_manager.login_view = 'auth.login'
login_manager.session_protection = 'basic'
login_manager.init_app(app)

Without secret_key nothing works, since the user id lives in the signed session cookie. Without login_view an unauthenticated request raises 401 instead of redirecting to your login page.

Load a user from a stored iddefine-user-loader

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

user_id always arrives as a string, so an integer primary key needs the int(). Return None for an unknown id; raising here turns a stale cookie into a 500 on every request.

Give your model the four required membersuser-model-contract

from flask_login import UserMixin

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String, unique=True)
    disabled = db.Column(db.Boolean, default=False)

    @property
    def is_active(self):
        return not self.disabled

UserMixin supplies is_authenticated, is_active, is_anonymous and get_id. Override is_active to represent suspended accounts, because login_user refuses an inactive user unless force=True.

Start a session after checking a passwordlog-user-in

from werkzeug.security import check_password_hash
from flask_login import login_user

user = User.query.filter_by(email=form_email).first()
if user and check_password_hash(user.password_hash, form_password):
    login_user(user, remember=form_remember)
    return redirect(url_for('dashboard'))
return render_template('login.html', error='Invalid credentials')

The password check is entirely yours; flask-login never sees it. Use the same generic error message for unknown user and wrong password so the form does not confirm which emails exist.

Redirect back safely after loginvalidate-next-redirect

from urllib.parse import urlparse

next_url = request.args.get('next')
if not next_url or urlparse(next_url).netloc:
    next_url = url_for('dashboard')
return redirect(next_url)

Nothing in the library checks this value. Redirecting straight to request.args['next'] is an open redirect, which is the single most common flask-login security bug.

Require a login for a viewprotect-view

from flask_login import login_required, current_user

@app.route('/settings')
@login_required
def settings():
    return render_template('settings.html', user=current_user)

Order matters: login_required must sit below the route decorator. OPTIONS requests are exempt by default, which keeps CORS preflight working.

Force reauthentication for sensitive actionsrequire-fresh-login

from flask_login import fresh_login_required, confirm_login

@app.route('/billing/card', methods=['POST'])
@fresh_login_required
def change_card():
    ...

@app.route('/reauth', methods=['POST'])
def reauth():
    if check_password_hash(current_user.password_hash, request.form['password']):
        confirm_login()
    return redirect(request.args.get('next') or url_for('dashboard'))

A session becomes non-fresh when it is restored from the remember-me cookie. Set login_manager.refresh_view or a needs_refresh_handler, otherwise the user gets a 401.

End the session and clear remember-melog-user-out

from flask_login import logout_user

@app.post('/logout')
@login_required
def logout():
    logout_user()
    session.clear()
    return redirect(url_for('index'))

logout_user removes the flask-login keys and cancels the remember cookie, but leaves everything else you put in the session. Make logout a POST so a link cannot log people out.

Fix the remember-me defaultsharden-remember-cookie

app.config.update(
    REMEMBER_COOKIE_SECURE=True,
    REMEMBER_COOKIE_SAMESITE='Lax',
    REMEMBER_COOKIE_DURATION=timedelta(days=14),
    SESSION_COOKIE_SECURE=True,
)

The shipped defaults are 365 days, Secure off and SameSite unset. HttpOnly is already on. None of these are changed for you in production mode.

Support an API token alongside sessionsauthenticate-from-header

@login_manager.request_loader
def load_from_request(request):
    token = request.headers.get('Authorization', '').removeprefix('Bearer ')
    if not token:
        return None
    return User.query.filter_by(api_token=token).first()

request_loader runs only when there is no session user, and it runs on every request, so keep the lookup cheap. The older header_loader is deprecated.

Return JSON instead of redirectingcustomise-unauthorized

@login_manager.unauthorized_handler
def unauthorized():
    if request.accept_mimetypes.best == 'application/json':
        return jsonify(error='authentication required'), 401
    return redirect(login_url('auth.login', request.url))

This replaces the redirect and the flashed message entirely. Use login_url to build the target so the next parameter is still attached.

Skip the login flow in teststest-as-logged-in-user

from flask_login import FlaskLoginClient

app.test_client_class = FlaskLoginClient

def test_settings_page(app, user):
    with app.test_client(user=user) as client:
        assert client.get('/settings').status_code == 200

Setting test_client_class is what enables the user= argument. Without it the argument is rejected and you are back to posting the login form in every test.

Alternatives

PackageRegistryPick it when
authlibPyPIYou need OAuth 2, OpenID Connect or JWT handling rather than a server-side login session
flask-security-tooPyPIYou want registration, password hashing, confirmation email, roles and 2FA as a package instead of writing them
fastapi-usersPyPIYou are choosing a stack fresh for an API and would rather have a maintained, batteries-included auth layer on FastAPI