Open-source project
maxcountryman/flask-login avatar
maxcountryman/flask-login

Flask-Login: session handling that deliberately refuses to own your password database

Flask user session management.

3,679 stars808 forksPythonMIT

At a glance

What is it?
Flask-Login manages login, logout and remembered sessions for Flask apps. It is not an authentication system: it holds no user store, hashes nothing, and leaves permissions entirely to you.
Who is it for?
Flask-Login is the right size for the job when your Flask app needs `@login_required` on some views and a logout link, and your users already live in a model you control. It stays out of the way on storage, which is its main appeal, and it does not pretend to be more than that.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three imports, a secret key and a login manager

The README's walkthrough is short enough to read in full, and it opens by being honest about its own scope. Flask-Login provides user session management for Flask, handling logging in, logging out and remembering sessions over extended periods. It is explicitly not bound to any particular database system or permissions model.

The setup is a Flask app, a secret key, and a `LoginManager` bound to the app.

python
import flask
import flask_login

app = flask.Flask(__name__)
app.secret_key = "super secret string"  # Change this!

login_manager = flask_login.LoginManager()
login_manager.init_app(app)

That comment on the secret key is the one line every reader should take literally. Flask signs the session cookie with it, so a hardcoded value in a repository is a session forgery waiting to happen. Generate one and load it from the environment.

The dependency list is deliberately short. `pyproject.toml` requires `flask>=2.3` and `werkzeug>=2.3.2`, and nothing else, with Python 3.8 or newer. The package is built with flit, declares itself a Beta development status, and takes `flask_login` as its module name.

The user_loader callback is the entire contract

Flask-Login's design reduces to one hook. Your user object subclasses `UserMixin` and exposes an id; the login manager gets a callback that turns an id back into a user, returning `None` when the id is unknown.

python
class User(flask_login.UserMixin):
    def __init__(self, email, password):
        self.id = email
        self.password = password

users = {"leafstorm": User("leafstorm", "secret")}
python
@login_manager.user_loader
def user_loader(id):
    return users.get(id)

The dictionary is obviously not a database, and the README says so, calling it a shortcut you should never take in a real application. It also makes the point that matters: it does not care how your data is stored so long as you tell it how to retrieve it.

That is why Flask-Login sits well next to SQLAlchemy, a NoSQL driver, or a directory service, and why the repository has no database in its requirements at all. The tree has no migrations directory and no ORM layer, just `src/`, `tests/` and `docs/`. Everything specific to your persistence choice lives in your code, and the only interface Flask-Login imposes is `get_id()` and the loader.

Four views that show where the security work is not happening

The example then defines a GET and a POST `/login`, a `/protected` view behind a decorator, and a `/logout` that clears the session.

python
@app.get("/login")
def login():
    return """<form method=post>
      Email: <input name="email"><br>
      Password: <input name="password" type=password><br>
      <button>Log In</button>
    </form>"""

@app.post("/login")
def login():
    user = users.get(flask.request.form["email"])

    if user is None or user.password != flask.request.form["password"]:
        return flask.redirect(flask.url_for("login"))

    flask_login.login_user(user)
    return flask.redirect(flask.url_for("protected"))

Read that comparison closely. `user.password != flask.request.form["password"]` compares a plaintext string from a dictionary to plaintext from a form. There is no hash, no salt, no work factor, and the example ships a user whose password is literally `secret`. The README's warning about shortcuts refers to this and to the hardcoded key above it.

The protected view shows the payoff, which is one decorator.

python
@app.route("/protected")
@flask_login.login_required
def protected():
    return flask.render_template_string(
        "Logged in as: {{ user.id }}",
        user=flask_login.current_user
    )

`login_required` sends an unauthenticated visitor to the login view and blocks the response otherwise, and `current_user` is available inside any template you render. That is the whole value proposition: three lines of wiring in exchange for a decorator you can apply anywhere. What it does not do is rate-limit the login endpoint, lock an account after repeated failures, require a second factor, or verify an OAuth token. Every one of those is your application, not the extension.

Remembering sessions and the limits of remembered sessions

The feature the README leads with after login and logout is remembering a session over extended periods, and it is the part with a security weight that is easy to underrate. A remembered session outlives the browser being closed, which means a cookie that survives a reboot on a shared machine. Flask-Login provides the mechanism, and the documentation on readthedocs.io covers its configuration.

The trade-off is inherent to the mechanism rather than to this package. A session cookie that dies with the browser is annoying; one that outlives it is a credential that has to be treated like a credential. The README does not discuss duration, secure or httponly cookie flags, or rotation, because Flask-Login sits above Flask's session handling and inherits those settings from the app and from the WSGI server configuration. If those are not set in your deployment, no decorator here will fix it.

The other limit worth naming is that this is session management, not authentication in the wider sense. It tracks who Flask currently believes is signed in. It has nothing to say about how that belief was formed, so the credential check, the lockout policy and the account recovery flow all live in your views.

Version numbers, ownership and what the README leaves out

Two details in the packaging metadata are worth reconciling. `pyproject.toml` carries version `0.7.0.dev`, while the most recent tagged release is 0.6.3, published 2023-10-30, whose description reads only "This fixes compatibility with Flask 3 and Werkzeug 3". A development marker on `main` ahead of the last tag is normal for a project using flit, so the practical reading is that 0.7.0 work is in progress and 0.6.3 is what you can install today. The release history before it is thin in the same way, with 0.6.2 from July 2022 and 0.6.1 from May 2022 pointing at `CHANGES.md` rather than repeating it.

The same block of metadata records Max Countryman as maintainer and Matthew Frazier as author, which reflects a change of hands for a package that many people have used for a decade. The repository records its last push on 2026-09-18, so the work is current even though the tags are not.

Development tooling is modern and strict, which says something about intent. Ruff handles linting with bugbear, pycodestyle, pyflakes, isort and pyupgrade rules and a 100 character line length. Pytest runs with `filterwarnings = ["error"]`, so any warning fails the suite, and coverage runs in branch mode over `flask_login` and `tests`. `tox.ini` and separate `requirements/` and `requirements-min/` directories are there to test against a range of Flask versions.

What the README does not contain is any coverage of remember-me configuration, `fresh_login_required`, anonymous user handling, or multi-login policies. All of that lives in the documentation at flask-login.readthedocs.io, which the README links to in its second line and never repeats.

Editorial conclusion

Flask-Login is the right size for the job when your Flask app needs `@login_required` on some views and a logout link, and your users already live in a model you control. It stays out of the way on storage, which is its main appeal, and it does not pretend to be more than that. What it will not give you is password hashing, brute-force protection, account lockout, OAuth or any kind of token validation, all of which the documented example skips with a comment telling you not to copy it. Install it with `pip install Flask-Login`, keep the secret key out of source, replace the example's dictionary with your real persistence layer, and register a `user_loader` that returns `None` for unknown ids.

Frequently asked questions

What is the Flask app secret key?

It is the value Flask uses to sign the session cookie, declared as `app.secret_key`. Flask-Login's own example sets it to the placeholder string `super secret string` with a comment telling you to change it, because a hardcoded or committed key lets anyone forge a signed session.

How do I install Flask-Login and log a user in?

Install the package, create a `LoginManager`, call `init_app(app)`, and register a `user_loader` callback that turns an id into a user. Wrap the views that need a session in `@login_required`. Logging in is then a single call to `flask_login.login_user(user)` once your own credential check passes, and `flask_login.logout_user()` ends the session.

Does Flask-Login store passwords or work with my database?

Neither, by design. The README states it is not bound to any database system or permissions model, and it hashes nothing. You supply a `user_loader` callback that turns an id back into a user object, and Flask-Login does not care whether that comes from Postgres, Mongo or a dictionary, as the documented example does.

Official sources

  1. License: MIT
  2. maxcountryman/flask-login on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/maxcountryman-flask-login.svg)](https://hysenlabs.com/projects/maxcountryman-flask-login)

Was this useful?