Rishabh Singh
HomeAboutServicesProjectsOpen SourceBlogVisitorsContact

v2025 · Next.js + Tailwind

All Posts
Home/Blog/Flask Session for Login
Read on Medium
PythonFlask

Flask Session for Login

Step-by-step session-based login in Flask: secret key, login route, protected route, logout — with full code.

April 6, 20236 min readRishabh Singh

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
"In production, load the secret key from an environment variable — never hardcode it."

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.

AspectClient-side signed cookie (Flask default)Server-side session (Flask-Session)
Where data livesIn the browser cookie, signedOn the server; browser holds only an ID
Size limit~4 KB (browser cookie cap)Effectively unlimited
Can the user read it?Yes — base64, not encryptedNo — data never leaves the server
Instant revocationNo — valid until expiryYes — delete the server record
Extra infrastructureNoneRedis / Memcached / database
Horizontal scalingTrivial — any server can verifyNeeds a shared session store
Best forSmall apps, simple login flagsSensitive 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.security or bcrypt
  • Session expiry: set app.permanent_session_lifetime and session.permanent = True to control how long sessions last
  • HTTPS only: set SESSION_COOKIE_SECURE = True in production so cookies are only sent over HTTPS
Reference: Flask Docs — Sessions
Back to BlogRead on Medium

© 2026 Rishabh Singh · Data Scientist & AI Engineer

PrivacyGitHubLinkedInMedium