Broken Authentication in Flask with Firestore
Broken Authentication in Flask with Firestore — how this specific combination creates or exposes the vulnerability
Broken Authentication occurs when identity management functions are implemented incorrectly, allowing attackers to compromise passwords, keys, or session tokens. In a Flask application using Google Cloud Firestore as the user store, several patterns specific to this stack can unintentionally weaken authentication security.
First, session management in Flask often relies on server-side sessions or signed cookies. If session identifiers or user IDs are stored directly in client-side cookies without integrity protection, an attacker who can steal or guess these values can impersonate other users. Firestore references (document paths or IDs) should never be exposed in URLs or tokens without additional verification, because an attacker can manipulate these references to access or escalate privileges across user boundaries (a BOLA/IDOR vector).
Second, password handling must be strict. Flask apps that use Firestore for user profiles must ensure passwords are hashed with a strong adaptive function such as bcrypt or Argon2. Storing plaintext or weakly hashed passwords in Firestore documents is a common misconfiguration. If the Firestore security rules rely only on user-supplied document IDs without server-side ownership checks, a malicious user may change another user’s document ID in requests and read or modify data.
Third, authentication endpoints must enforce rate limiting and account lockout to prevent credential stuffing or brute-force attacks. Without explicit rate limiting on login routes, attackers can probe credentials at scale. Firestore queries that filter by email must be deterministic and avoid leaking existence via timing differences; naive implementations that return different HTTP status codes or response shapes for missing users can enable enumeration.
Fourth, the use of custom claims in ID tokens must be carefully managed. If Flask sets or trusts custom claims from unverified sources or uses Firestore data to directly influence token claims without validation, attackers may escalate privileges by modifying claims or session data. Always verify the token on the backend and ensure Firestore-based authorization checks are independent of client-supplied data.
Finally, insecure direct object references occur when a Flask route uses a user-provided identifier to fetch a Firestore document without confirming that the authenticated user owns that document. For example, a route like /users/<user_id>/profile that directly uses user_id from the request without matching it to the authenticated UID allows horizontal privilege escalation. The combination of Flask’s flexible routing and Firestore’s straightforward document paths makes it easy to accidentally expose references that should be guarded.
Firestore-Specific Remediation in Flask — concrete code fixes
To secure authentication in Flask with Firestore, apply server-side checks, strong cryptography, and strict session handling. Below are concrete, defensive patterns with real Firestore code examples.
Password storage and verification
Store passwords using bcrypt. Never store plaintext or reversible hashes in Firestore.
import bcrypt
from google.cloud import firestore
db = firestore.Client()
def create_user(email: str, password: str):
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
user_ref = db.collection('users').document(email)
user_ref.set({
'email': email,
'password_hash': hashed.decode('utf-8'), # store as string
'created_at': firestore.SERVER_TIMESTAMP,
})
def verify_user(email: str, password: str) -> bool:
user_ref = db.collection('users').document(email)
doc = user_ref.get()
if not doc.exists:
return False
stored = doc.to_dict()['password_hash'].encode('utf-8')
return bcrypt.checkpw(password.encode('utf-8'), stored)
Secure session and token handling
Use signed Flask sessions and avoid storing sensitive identifiers in the session payload. Always validate ownership on each request.
from flask import Flask, session, request
from google.cloud import firestore
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_bytes(32)
db = firestore.Client()
@app.route('/login', methods=['POST'])
def login():
email = request.form.get('email')
password = request.form.get('password')
if not verify_user(email, password):
return {'error': 'Invalid credentials'}, 401
session['user_email'] = email # minimal session data
return {'ok': True}
@app.route('/profile')
def profile():
user_email = session.get('user_email')
if not user_email:
return {'error': 'Unauthorized'}, 401
user_ref = db.collection('users').document(user_email)
doc = user_ref.get()
if not doc.exists:
return {'error': 'User not found'}, 404
# Return only safe, non-sensitive fields
return {k: v for k, v in doc.to_dict().items() if k != 'password_hash'}
Authorization with BOLA/IDOR protection
Always verify that the authenticated user owns the target resource. Use the authenticated UID (or email) to scope Firestore queries.
@app.route('/users/<user_id>/profile')
def get_user_profile(user_id: str):
user_email = session.get('user_email')
if not user_email:
return {'error': 'Unauthorized'}, 401
# Ensure the requested user_id matches the authenticated user
if user_id != user_email: # or map user_id to email safely
return {'error': 'Forbidden'}, 403
user_ref = db.collection('users').document(user_email)
doc = user_ref.get()
if not doc.exists:
return {'error': 'Not found'}, 404
return {k: v for k, v in doc.to_dict().items() if k != 'password_hash'}
Rate limiting and existence-oblivious responses
Apply rate limiting at the Flask level and use consistent response shapes to avoid user enumeration via timing or status code differences.
from flask_limiter import Limiter
limiter = Limiter(app=app, key_func=lambda: session.get('user_email') or request.remote_addr)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login_limited():
# same verify logic as above; always return the same shape
email = request.form.get('email')
password = request.form.get('password')
# Always perform hash check to avoid timing leaks, even if email not found
verify_user(email, password)
return {'ok': True}
Firestore security rules mindset
While Firestore rules are not a substitute for backend checks, design them to enforce ownership. In production, pair server-side authorization with rules that require authentication and match request.auth.uid with document data. In Flask, always perform your own ownership checks; do not rely solely on rules for authorization.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |