The Anatomy of Authentication
Authentication answers 'who is this user?' and in a Flask application it typically involves three pieces: a User model with a securely hashed password (never plaintext), a login route that verifies submitted credentials against the stored hash, and a mechanism — usually Flask's signed session cookie — that remembers the authenticated user across subsequent requests. Flask itself has no built-in authentication system; it provides the primitives (sessions, request handling) that extensions like Flask-Login or hand-rolled code build upon.
Cricket analogy: Authentication is like a stadium's accreditation check: a steward verifies your credential badge (hashed password check) once at the gate, then issues a wristband (session cookie) that gets you back in through any entrance for the rest of the day without re-verifying every time.
Hashing Passwords Correctly
Passwords must never be stored in plaintext or with fast general-purpose hashes like MD5 or SHA-256 alone, because those are too fast to brute-force at scale. Werkzeug (a Flask dependency) provides generate_password_hash(), which by default uses a strong, salted algorithm (scrypt in modern Werkzeug versions), and check_password_hash() to verify a plaintext attempt against the stored hash without ever needing to reverse it. A salt embedded in the hash ensures that two users with the same password produce different stored hashes, defeating precomputed rainbow-table attacks.
Cricket analogy: Salting a password hash is like each ground having its own unique pitch conditions, so a bowling strategy (attack) that works on a flat Chepauk pitch won't transfer to a bouncy WACA pitch — a precomputed attack against one user's hash doesn't help against another's differently salted hash.
from flask import Flask, request, session, redirect, url_for, jsonify
from werkzeug.security import generate_password_hash, check_password_hash
app = Flask(__name__)
app.secret_key = 'use-a-long-random-value-from-os.urandom(32)'
users = {
'ada': generate_password_hash('correct-horse-battery-staple')
}
@app.route('/login', methods=['POST'])
def login():
data = request.get_json(silent=True) or {}
username = data.get('username')
password = data.get('password')
stored_hash = users.get(username)
if stored_hash is None or not check_password_hash(stored_hash, password):
return jsonify({'error': 'Invalid credentials'}), 401
session['user_id'] = username
session.permanent = True
return jsonify({'message': f'Welcome, {username}'}), 200
@app.route('/logout', methods=['POST'])
def logout():
session.pop('user_id', None)
return jsonify({'message': 'Logged out'}), 200
@app.route('/profile')
def profile():
if 'user_id' not in session:
return jsonify({'error': 'Not authenticated'}), 401
return jsonify({'user': session['user_id']}), 200Verifying Credentials and Establishing Identity
On login, the view looks up the user record by username, calls check_password_hash(stored_hash, submitted_password) to compare securely (using a constant-time comparison internally to resist timing attacks), and, only on success, writes an identifier into Flask's session — never the password itself. Every subsequent request that includes the browser's signed session cookie can then check session['user_id'] to determine whether the caller is authenticated, without hitting the database on every request just to check a login flag.
Cricket analogy: Constant-time comparison in check_password_hash is like a third umpire taking exactly the same review time whether the decision is obviously out or a tight call, so no one can infer the answer just from how long the review takes — timing attacks are defeated the same way.
Never store a password reset token or session identifier that reveals information about the password itself. Store only opaque, unguessable identifiers (like a user ID or a securely random token), and always regenerate the session (e.g. by clearing and rebuilding it) after a successful login to prevent session fixation attacks.
Common Pitfalls
A frequent mistake is comparing passwords with Python's == operator on raw hashes or, worse, storing passwords in reversible encryption instead of a one-way hash — both undermine the entire point of hashing. Another common issue is failing to set app.secret_key to a long, random, environment-provided value, which leaves the session cookie's signature forgeable; a hardcoded or short secret_key checked into source control is a critical vulnerability that lets an attacker forge arbitrary session data, including admin login state.
Cricket analogy: Comparing hashes with == instead of a timing-safe function is like judging a run-out purely by eyeballing it from the boundary rather than using the specialized snickometer review process — it looks similar but skips the safeguard that actually protects against a wrong call.
Never hardcode app.secret_key as a literal string in source code committed to version control. Load it from an environment variable (e.g. os environ.get('FLASK_SECRET_KEY')) generated with something like secrets.token_hex(32), and rotate it if it's ever exposed.
- Flask has no built-in authentication system; it provides sessions and request handling that authentication logic builds on top of.
- Always hash passwords with a strong algorithm like Werkzeug's generate_password_hash() (scrypt by default) — never store plaintext.
- Use check_password_hash() for verification; it performs a constant-time comparison to resist timing attacks.
- On successful login, store only a non-sensitive identifier (like a user ID) in the session, never the password.
- Set app.secret_key to a long, random value loaded from environment configuration, never hardcoded in source.
- Regenerate or rebuild the session after login to prevent session fixation attacks.
- Check session state on protected routes to determine whether a request is authenticated.
Practice what you learned
1. Why shouldn't passwords be hashed with a fast algorithm like plain SHA-256 alone?
2. What Werkzeug function is used to verify a submitted password against a stored hash?
3. What should be stored in the Flask session after a successful login?
4. Why is a weak or hardcoded app.secret_key a serious vulnerability?
5. What is a salt's purpose in password hashing?
Was this page helpful?
You May Also Like
Flask-Login Explained
Learn how the Flask-Login extension manages user sessions, the UserMixin interface, and route protection with login_required.
Session Management in Flask
Understand how Flask's signed cookie-based sessions work, their limitations, and how to configure them securely.
Building REST APIs with Flask
Learn how to design and implement RESTful APIs in Flask using route decorators, HTTP verbs, and JSON responses.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics