Flask Session for Login
Step-by-step session-based login in Flask: secret key, login route, protected route, logout — with full code.
A Flask session is a per-user dictionary that survives across requests by storing its contents in a cryptographically signed cookie in the user's browser.
Flask serializes the session dict to JSON, signs it with your application's secret_key using HMAC (via the itsdangerous library), and sends the result as the session cookie.
On every subsequent request, Flask verifies the signature before trusting the data — the user can read the cookie's contents, but cannot modify them without breaking the signature.
That property makes sessions the simplest correct way to build a login system: after validating credentials once, set session['logged_in'] = True, and every protected route can check that flag instead of re-authenticating.
A complete login flow needs exactly four pieces — a secret key, a login route, a protected route, and a logout route — all shown below in under thirty lines of code.
How Flask Sessions Work
Flask sessions store data in a signed cookie on the user's browser.
The server signs the cookie with a secret key using HMAC — users can read the cookie but cannot modify it without the signature breaking.
On each request, Flask reads the cookie, verifies the signature, and exposes the data through the session object.
For login, you store a flag like session['logged_in'] = True after credentials check out. Every protected route then checks this flag before serving content.
If your frontend runs on a different origin (a React dev server, for instance), the session cookie only survives cross-origin requests when CORS is configured with supports_credentials.
How Does Flask Sign Session Cookies?
Under the hood, Flask (including Flask 3.x) delegates signing to itsdangerous, the same library that powers password-reset tokens.
The session dict is JSON-serialized, compressed if it helps, base64-encoded, and then signed with an HMAC derived from your secret_key.
The resulting cookie has three dot-separated parts:
# session cookie anatomy:
# eyJsb2dnZWRfaW4iOnRydWV9.aGVsbG8xMjM.q2V4mJx8...
# |______ payload ______| |timestamp| |signature|
# base64-encoded JSON issued at HMAC of the rest
One detail trips people up: the payload is encoded, not encrypted. Anyone holding the cookie can decode and read it:
import base64, json
payload = 'eyJsb2dnZWRfaW4iOnRydWV9'
print(json.loads(base64.urlsafe_b64decode(payload + '==')))
# {'logged_in': True}
So never put secrets — passwords, API keys, personal data — in the session.
The signature guarantees integrity (a tampered cookie is rejected and the session comes back empty), not confidentiality.
It also means anyone who obtains your secret_key can forge a valid logged_in: true cookie, which is why the key must be long, random, and out of version control.
Step 1: Set a Secret Key
The secret key signs session cookies. Without it, Flask raises a RuntimeError the moment you touch session.
from flask import Flask, session, request, redirect, render_template
app = Flask(__name__)
app.secret_key = 'your_secret_key_here' # use a long random string in production
Step 2: Define a Login Route
Handle both GET (show the form) and POST (validate credentials) — see HTTP methods for when each verb applies. On success, set the session flag and redirect to the protected page.
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' and password == 'password':
session['logged_in'] = True
return redirect('/')
return render_template('login.html')
Step 3: Define a Protected Route
Check session.get('logged_in') before serving protected content. Redirect unauthenticated users to login.
@app.route('/')
def index():
if session.get('logged_in'):
return render_template('index.html')
return redirect('/login')
Step 4: Define a Logout Route
session.clear() removes all session data, effectively logging the user out.
@app.route('/logout')
def logout():
session.clear()
return redirect('/login')
if __name__ == '__main__':
app.run(debug=True)
How Do You Protect Many Routes at Once?
Repeating the session.get('logged_in') check in every view gets tedious fast.
The idiomatic fix is a login_required decorator — write the check once, then stack it on any route that needs authentication:
from functools import wraps
from flask import session, redirect
def login_required(view):
@wraps(view)
def wrapped(*args, **kwargs):
if not session.get('logged_in'):
return redirect('/login')
return view(*args, **kwargs)
return wrapped
@app.route('/dashboard')
@login_required
def dashboard():
return render_template('dashboard.html')
The @wraps call preserves the view function's name so Flask's routing and debugging tools keep working.
This is exactly the pattern the Flask-Login extension formalizes — once your app outgrows a single flag, it adds user objects, remember-me cookies, and per-user sessions on top of the same idea.
Should You Use Server-Side Sessions Instead?
Flask's default client-side cookie is perfect for small apps, but it has structural limits: browsers cap cookies at roughly 4 KB, the data rides along on every request, and you cannot revoke a session from the server — a signed cookie stays valid until it expires. Server-side sessions (via the Flask-Session extension backed by Redis, Memcached, or a database) store the data on the server and give the browser only an opaque session ID.
| Aspect | Client-side signed cookie (Flask default) | Server-side session (Flask-Session) |
|---|---|---|
| Where data lives | In the browser cookie, signed | On the server; browser holds only an ID |
| Size limit | ~4 KB (browser cookie cap) | Effectively unlimited |
| Can the user read it? | Yes — base64, not encrypted | No — data never leaves the server |
| Instant revocation | No — valid until expiry | Yes — delete the server record |
| Extra infrastructure | None | Redis / Memcached / database |
| Horizontal scaling | Trivial — any server can verify | Needs a shared session store |
| Best for | Small apps, simple login flags | Sensitive data, force-logout, large sessions |
Rule of thumb: if the session holds nothing but a login flag and a username, the default signed cookie is simpler and scales better. Reach for server-side sessions when you need to store more than 4 KB, keep data hidden from the user, or terminate sessions on demand (for example, "log out all devices").
Video Walkthroughs
Security Notes
- Secret key: use a cryptographically random value (e.g.,
secrets.token_hex(32)) and load it from an environment variable - Credential validation: never compare plain-text passwords — hash with
werkzeug.securityorbcrypt - Session expiry: set
app.permanent_session_lifetimeandsession.permanent = Trueto control how long sessions last - HTTPS only: set
SESSION_COOKIE_SECURE = Truein production so cookies are only sent over HTTPS