User Login with Flask-Login
Reviewed & published by Brayan K
Flask-Login is an extension that manages user sessions, storing the logged-in user's identity in a signed cookie so your app remembers who someone is across requests.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll build the full login flow — verifying credentials, calling login_user, protecting routes with @login_required, and reading current_user in your views.
What You'll Learn in This Lesson
- How a session-cookie login flow works end to end
- Adding UserMixin to your User model
- Configuring LoginManager and a user_loader
- Calling login_user and logout_user
- Protecting routes with @login_required
- Reading the logged-in user with current_user
1 The Login Flow, Stripped to Its Core
Before reaching for the extension, it helps to see what it does under the hood. A login is just this: verify a password, then remember the user's identity in the session (a signed cookie). Every later request reads that cookie to know who is logged in.
The runnable example below builds that flow with plain Flask session. A /login route checks credentials and stores the username; /dashboard is "protected" by checking the session; /logout clears it. This is exactly the machinery Flask-Login automates for you.
# app.py — the login flow with core Flask session (runnable)
from flask import Flask, session, request
app = Flask(__name__)
app.secret_key = "demo-secret-key" # signs the session cookie
USERS = {"ada": "password123"} # pretend database
@app.route("/login", methods=["POST"])
def login():
user = request.form.get("username")
pw = request.form.get("password")
if USERS.get(user) == pw: # verify credentials
session["user"] = user # remember the user in the cookie
return f"Logged in as {user}"
return "Invalid credentials", 401
@app.route("/dashboard")
def dashboard():
if "user" not in session: # this is what @login_required checks
return "Please log in", 401
return f"Welcome to your dashboard, {session['user']}!"
@app.route("/logout")
def logout():
session.pop("user", None) # forget the user
return "Logged out"
# Simulate a full session with the test client (cookies persist across calls):
with app.test_client() as client:
print(client.get("/dashboard").status_code) # 401
print(client.post("/login", data={"username": "ada", "password": "password123"}).data.decode())
print(client.get("/dashboard").data.decode())
print(client.get("/logout").data.decode())
print(client.get("/dashboard").status_code) # 401 again
# ✅ Expected output:
# 401
# Logged in as ada
# Welcome to your dashboard, ada!
# Logged out
# 401Output: 401, then Logged in as ada, then Welcome to your dashboard, ada!, then Logged out, then 401 again — the session cookie is what carries the identity between requests.
2 Wiring Up Flask-Login: UserMixin & LoginManager
The real extension replaces that manual bookkeeping with three pieces. First, your User model inherits from UserMixin, which supplies the properties Flask-Login expects. Second, a LoginManager is attached to the app. Third, a @login_manager.user_loader function tells Flask-Login how to fetch a user by ID on each request.
Here is the production setup. It is shown read-only because it relies on the flask_login package, but it maps one-to-one onto the runnable flow above.
3 Protecting Routes & Reading current_user
With Flask-Login, @login_required wraps any view and returns a redirect (or 401) when no one is logged in. Inside a protected view, current_user is the live User object — you can read current_user.username, check current_user.is_authenticated, or branch on a role.
The runnable demo below reproduces @login_required and current_user with a tiny decorator over the core session, so you can watch a protected route reject an anonymous client and then greet a logged-in one.
# app.py — a login_required-style guard and a current_user stand-in (runnable)
from functools import wraps
from flask import Flask, session, request, g
app = Flask(__name__)
app.secret_key = "demo-secret-key"
USERS = {"ada": {"name": "Ada Lovelace", "role": "admin"}}
def login_required(view):
@wraps(view)
def wrapper(*args, **kwargs):
if "user" not in session:
return "Login required", 401
g.current_user = USERS[session["user"]] # like Flask-Login's current_user
return view(*args, **kwargs)
return wrapper
@app.route("/login", methods=["POST"])
def login():
if USERS.get(request.form.get("username")):
session["user"] = request.form["username"]
return "ok"
return "no such user", 401
@app.route("/account")
@login_required
def account():
return f"Account for {g.current_user['name']} (role: {g.current_user['role']})"
with app.test_client() as client:
print(client.get("/account").status_code) # 401 (not logged in)
client.post("/login", data={"username": "ada"})
print(client.get("/account").data.decode()) # uses current_user
# ✅ Expected output:
# 401
# Account for Ada Lovelace (role: admin)Output: 401 for the anonymous request, then Account for Ada Lovelace (role: admin) once logged in. Swap g.current_user for Flask-Login's current_user and the code reads identically.
🎯 Your Turn: Fill in the Blanks
Complete the login flow. Replace each ___ so the user is remembered, the protected route checks the session, and logout clears it.
# 🎯 YOUR TURN: replace each ___ to finish the login flow
from flask import Flask, session, request
app = Flask(__name__)
app.secret_key = "demo-secret-key"
USERS = {"ada": "pw"}
@app.route("/login", methods=["POST"])
def login():
u, p = request.form.get("username"), request.form.get("password")
if USERS.get(u) == p:
session["___"] = u # remember the user in the session
return "logged in"
return "bad", 401
@app.route("/secret")
def secret():
if "user" ___ session: # which operator checks membership?
return "Please log in", 401
return f"Secret for {session['user']}"
@app.route("/logout")
def logout():
session.___("user", None) # remove the key from the session
return "logged out"
with app.test_client() as c:
c.post("/login", data={"username": "ada", "password": "pw"})
print(c.get("/secret").data.decode()) # Secret for ada
c.get("/logout")
print(c.get("/secret").status_code) # 401Common Errors & Quick Reference
❌ "Missing user_loader" or current_user is always anonymous
You forgot the @login_manager.user_loader callback. Without it, Flask-Login cannot turn the id in the cookie back into a user, so current_user stays anonymous.
❌ RuntimeError: The session is unavailable / no secret key
Sessions are signed, so you must set app.secret_key. Without it, Flask cannot store or read the login cookie and raises an error.
Never store plaintext. Hash on registration with generate_password_hash and verify with check_password_hash before calling login_user.
| Task | Code |
|---|---|
| Start a session | login_user(user) |
| End a session | logout_user() |
| Protect a route | @login_required |
| Read the user | current_user.username |
🏆 Mini Challenge: Role-Based Access
Build an admin-only route that authorizes on top of authentication.
- Log a user in and store their role in the session
- Protect /admin so anonymous users get 401
- Return 403 if the logged-in user is not an admin
# 🏆 Mini Challenge: authentication + authorization
from flask import Flask, session, request
app = Flask(__name__)
app.secret_key = "demo-secret-key"
USERS = {"ada": "admin", "bob": "member"} # username -> role
@app.route("/login", methods=["POST"])
def login():
u = request.form.get("username")
if u in USERS:
session["user"] = u
session["role"] = USERS[u]
return "ok"
return "no", 401
@app.route("/admin")
def admin():
if "user" not in session:
return "Login required", 401 # not authenticated
if session.get("role") != "admin":
return "Forbidden", 403 # authenticated but not authorized
return f"Admin panel for {session['user']}"
with app.test_client() as c:
print(c.get("/admin").status_code) # 401
c.post("/login", data={"username": "bob"})
print(c.get("/admin").status_code) # 403
c.post("/login", data={"username": "ada"})
print(c.get("/admin").data.decode()) # Admin panel for ada❓ Frequently Asked Questions
Lesson complete — your app can recognize its users!
You now understand the session-cookie login flow, how UserMixin and the user_loader power current_user, and how @login_required guards your routes.
Practice quiz
What does Flask-Login primarily manage for you?
- database migrations
- the user session and current_user
- HTML templates
- URL routing
Answer: the user session and current_user. It stores the user's id in the session and reloads them via current_user on each request.
Which function logs a user in?
- login_user(user)
- user.login()
- session.login(user)
- authenticate(user)
Answer: login_user(user). login_user(user) stores user.get_id() in the signed session cookie.
Which decorator blocks anonymous visitors from a route?
- @require_auth
- @authenticated
- @login_required
- @protected
Answer: @login_required. @login_required redirects or returns 401 when no user is logged in.
How do you read the currently logged-in user?
- session['user']
- request.user
- g.user
- current_user
Answer: current_user. Flask-Login exposes current_user, the live User object, in views and templates.
What does UserMixin provide on your User model?
- is_authenticated, is_active, is_anonymous, get_id()
- a database table
- password hashing
- the login form HTML
Answer: is_authenticated, is_active, is_anonymous, get_id(). UserMixin supplies the properties Flask-Login expects from a user object.
What does the @login_manager.user_loader callback do?
- renders the login page
- loads a user by id from the session on each request
- hashes the password
- creates the session cookie
Answer: loads a user by id from the session on each request. user_loader turns the id stored in the cookie back into a full User object.
Which function ends the user's session?
- session.end()
- current_user.logout()
- logout_user()
- login_user(None)
Answer: logout_user(). logout_user() clears the logged-in user from the session.
What is stored in the cookie when you call login_user(user)?
- the password hash
- the whole User object
- the username only
- user.get_id()
Answer: user.get_id(). Only the user's id is stored; load_user rebuilds the full object each request.
How should passwords be verified at login?
- check_password_hash against a stored hash
- compare raw strings with ==
- store them in the session
- encode with base64
Answer: check_password_hash against a stored hash. Hash on registration with generate_password_hash and verify with check_password_hash.
What is the difference between authentication and authorization?
- they are the same thing
- authentication is 'who are you', authorization is 'are you allowed'
- authorization happens first
- authentication checks roles
Answer: authentication is 'who are you', authorization is 'are you allowed'. Authentication verifies identity; authorization checks permissions once identity is known.
Continue this course
- Previous: Checkpoint: A Dynamic Web App
- Next: Form Validation with Flask-WTF — Validate forms and block bad input with WTForms validators and CSRF
- Quick reference: Flask cheat sheet
Frequently asked questions
What does Flask-Login actually do?
Flask-Login manages the user session for you. After you call login_user(user), it stores the user's ID in a signed session cookie, reloads that user on every request through your user_loader, and exposes current_user everywhere. It also provides the @login_required decorator to block anonymous visitors.
What is UserMixin and why do I need it?
UserMixin is a small class you inherit from on your User model. It supplies the four properties Flask-Login expects — is_authenticated, is_active, is_anonymous, and get_id() — so you do not have to write them yourself. Without them, login_user would not know how to identify or persist your user.
What is the difference between authentication and authorization?
Authentication answers 'who are you?' — verifying a username and password and starting a session. Authorization answers 'are you allowed to do this?' — checking roles or permissions once you know who the user is. Flask-Login handles authentication; you build authorization on top with checks like current_user.is_admin.
Where should I check the password?
Never store or compare raw passwords. Save a hash with werkzeug.security.generate_password_hash when the user registers, and verify it with check_password_hash at login. Only after the hash matches do you call login_user.