Authentication & Session Security
Learn secure authentication practices: password handling, session management, token security, and common vulnerabilities.
Authentication verifies identity. Session management maintains that identity across requests. Both are critical security functions, and mistakes in either can compromise your entire application. This section covers secure implementation patterns for passwords, sessions, and tokens.
Secure Password Handling
Never Store Plain Text Passwords
This should be obvious, but breaches still happen:
# NEVER DO THIS
user.password = request.form['password'] # Plain text storage
# Also never do this
user.password = hashlib.md5(password.encode()).hexdigest() # Weak hash, no salt
user.password = hashlib.sha256(password.encode()).hexdigest() # No salt
Use Modern Password Hashing
Use algorithms specifically designed for password hashing:
# RECOMMENDED: Use bcrypt, Argon2, or scrypt
import bcrypt
def hash_password(password: str) -> str:
"""Hash a password for storage."""
# bcrypt automatically generates a salt and includes it in the hash
salt = bcrypt.gensalt(rounds=12) # Work factor of 12
return bcrypt.hashpw(password.encode(), salt).decode()
def verify_password(password: str, password_hash: str) -> bool:
"""Verify a password against its hash."""
return bcrypt.checkpw(password.encode(), password_hash.encode())
# Or use Argon2 (winner of Password Hashing Competition)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64 MB memory usage
parallelism=4 # Number of parallel threads
)
def hash_password_argon2(password: str) -> str:
return ph.hash(password)
def verify_password_argon2(password: str, hash: str) -> bool:
try:
ph.verify(hash, password)
return True
except:
return False
Why these algorithms?
- Slow by design - Makes brute-force attacks expensive
- Memory-hard (Argon2) - Resists GPU/ASIC attacks
- Automatic salting - Prevents rainbow table attacks
- Adjustable work factor - Can increase as hardware gets faster
Password Policy Enforcement
import re
from zxcvbn import zxcvbn # Password strength estimator
class PasswordPolicy:
MIN_LENGTH = 12
MAX_LENGTH = 128
MIN_STRENGTH_SCORE = 3 # zxcvbn score 0-4
@classmethod
def validate(cls, password: str, user_inputs: list = None) -> tuple[bool, list[str]]:
"""Validate password against policy. Returns (valid, errors)."""
errors = []
if len(password) < cls.MIN_LENGTH:
errors.append(f"Password must be at least {cls.MIN_LENGTH} characters")
if len(password) > cls.MAX_LENGTH:
errors.append(f"Password must be at most {cls.MAX_LENGTH} characters")
# Check against common passwords and user-specific data
result = zxcvbn(password, user_inputs=user_inputs or [])
if result['score'] < cls.MIN_STRENGTH_SCORE:
errors.append(f"Password is too weak: {result['feedback']['warning']}")
if result['feedback']['suggestions']:
errors.extend(result['feedback']['suggestions'])
return len(errors) == 0, errors
# Usage
valid, errors = PasswordPolicy.validate(
password,
user_inputs=[user.email, user.username, user.name] # Penalize passwords similar to user data
)
Password Reset Security
import secrets
from datetime import datetime, timedelta
def create_password_reset_token(user):
"""Generate a secure password reset token."""
# Use cryptographically secure random token
token = secrets.token_urlsafe(32)
# Store hash of token, not the token itself
token_hash = hashlib.sha256(token.encode()).hexdigest()
user.reset_token_hash = token_hash
user.reset_token_expires = datetime.utcnow() + timedelta(hours=1)
db.session.commit()
return token # Send this to user via email
def verify_reset_token(user, token):
"""Verify a password reset token."""
if not user.reset_token_hash or not user.reset_token_expires:
return False
if datetime.utcnow() > user.reset_token_expires:
return False
token_hash = hashlib.sha256(token.encode()).hexdigest()
if not secrets.compare_digest(token_hash, user.reset_token_hash):
return False
return True
def complete_password_reset(user, token, new_password):
"""Complete password reset and invalidate token."""
if not verify_reset_token(user, token):
raise ValidationError("Invalid or expired reset token")
user.password_hash = hash_password(new_password)
user.reset_token_hash = None
user.reset_token_expires = None
# Invalidate all existing sessions
invalidate_all_sessions(user)
db.session.commit()
Session Security
Secure Session Configuration
# Flask example
app.config.update(
SECRET_KEY=os.environ['SECRET_KEY'], # At least 32 bytes of randomness
SESSION_COOKIE_SECURE=True, # Only send over HTTPS
SESSION_COOKIE_HTTPONLY=True, # Not accessible via JavaScript
SESSION_COOKIE_SAMESITE='Lax', # CSRF protection
PERMANENT_SESSION_LIFETIME=timedelta(hours=24),
)
# Express.js example
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // HTTPS only
httpOnly: true, // No JavaScript access
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
Session ID Security
import secrets
def generate_session_id():
"""Generate a cryptographically secure session ID."""
# At least 128 bits of randomness
return secrets.token_hex(32) # 256 bits = 64 hex characters
def regenerate_session(old_session):
"""Regenerate session ID after privilege change."""
# Copy important data
user_id = old_session.get('user_id')
# Clear old session
old_session.clear()
# Create new session with new ID
new_session_id = generate_session_id()
old_session['user_id'] = user_id
old_session.modified = True
return new_session_id
Regenerate session ID after:
- Login (prevent session fixation)
- Privilege escalation (e.g., entering admin area)
- Password change
Session Fixation Prevention
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form['username'], request.form['password'])
if user:
# IMPORTANT: Regenerate session to prevent fixation
session.clear()
session['user_id'] = user.id
session['created_at'] = datetime.utcnow().isoformat()
session['ip_address'] = request.remote_addr
session.regenerate() # Framework-specific method
return redirect('/dashboard')
return render_template('login.html', error='Invalid credentials')
Token-Based Authentication (JWT)
Secure JWT Implementation
import jwt
from datetime import datetime, timedelta
JWT_SECRET = os.environ['JWT_SECRET'] # At least 256 bits
JWT_ALGORITHM = 'HS256'
ACCESS_TOKEN_EXPIRE_MINUTES = 15
REFRESH_TOKEN_EXPIRE_DAYS = 7
def create_access_token(user_id: int) -> str:
"""Create a short-lived access token."""
payload = {
'sub': str(user_id),
'type': 'access',
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
'jti': secrets.token_hex(16) # Unique token ID
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def create_refresh_token(user_id: int) -> str:
"""Create a longer-lived refresh token."""
token_id = secrets.token_hex(16)
payload = {
'sub': str(user_id),
'type': 'refresh',
'iat': datetime.utcnow(),
'exp': datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS),
'jti': token_id
}
# Store token ID in database for revocation
RefreshToken.create(user_id=user_id, token_id=token_id)
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def verify_token(token: str, expected_type: str) -> dict:
"""Verify and decode a JWT token."""
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
if payload.get('type') != expected_type:
raise jwt.InvalidTokenError("Invalid token type")
# For refresh tokens, check if revoked
if expected_type == 'refresh':
if not RefreshToken.exists(payload['jti']):
raise jwt.InvalidTokenError("Token revoked")
return payload
except jwt.ExpiredSignatureError:
raise AuthenticationError("Token expired")
except jwt.InvalidTokenError as e:
raise AuthenticationError(f"Invalid token: {e}")
JWT Security Considerations
# NEVER do these:
# 1. Don't use 'none' algorithm
jwt.decode(token, options={"verify_signature": False}) # DANGEROUS
# 2. Don't accept algorithm from token header
header = jwt.get_unverified_header(token)
jwt.decode(token, secret, algorithms=[header['alg']]) # Algorithm confusion attack
# 3. Don't store sensitive data in JWT (it's base64, not encrypted)
payload = {'user_id': 1, 'password': 'secret'} # NEVER
# 4. Don't use weak secrets
JWT_SECRET = 'secret' # Too short and predictable
# ALWAYS:
# - Use strong secrets (>= 256 bits)
# - Specify allowed algorithms explicitly
# - Keep access tokens short-lived
# - Implement token revocation for refresh tokens
Multi-Factor Authentication (MFA)
TOTP Implementation
import pyotp
import qrcode
from io import BytesIO
import base64
def setup_totp(user):
"""Generate TOTP secret for user."""
# Generate a random secret
secret = pyotp.random_base32()
# Store encrypted secret (encrypt at rest)
user.totp_secret = encrypt(secret)
user.mfa_enabled = False # Not enabled until verified
db.session.commit()
# Generate provisioning URI for authenticator apps
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name=user.email,
issuer_name="MyApp"
)
# Generate QR code
qr = qrcode.make(uri)
buffer = BytesIO()
qr.save(buffer, format='PNG')
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
return {
'secret': secret, # Show once for manual entry
'qr_code': f"data:image/png;base64,{qr_base64}"
}
def verify_totp(user, code):
"""Verify a TOTP code."""
if not user.totp_secret:
return False
secret = decrypt(user.totp_secret)
totp = pyotp.TOTP(secret)
# valid_window allows for clock skew (1 = 30 seconds before/after)
return totp.verify(code, valid_window=1)
Backup Codes
def generate_backup_codes(user, count=10):
"""Generate one-time backup codes for MFA recovery."""
codes = [secrets.token_hex(4).upper() for _ in range(count)]
# Store hashed codes
user.backup_codes = [
hashlib.sha256(code.encode()).hexdigest()
for code in codes
]
db.session.commit()
# Return plain codes to user (show once)
return codes
def use_backup_code(user, code):
"""Verify and consume a backup code."""
code_hash = hashlib.sha256(code.upper().encode()).hexdigest()
if code_hash in user.backup_codes:
user.backup_codes.remove(code_hash)
db.session.commit()
return True
return False
Rate Limiting Authentication
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute") # Strict limit on login
def login():
# ... authentication logic
pass
@app.route('/api/password-reset', methods=['POST'])
@limiter.limit("3 per hour") # Very strict for password reset
def request_password_reset():
# ... password reset logic
pass
Key Takeaways
- Use modern password hashing - Argon2 or bcrypt, never MD5/SHA alone
- Secure session cookies - HttpOnly, Secure, SameSite flags
- Regenerate sessions - After login and privilege changes
- Keep access tokens short-lived - 15 minutes or less
- Implement token revocation - Especially for refresh tokens
- Add rate limiting - Prevent brute force attacks
- Support MFA - TOTP with backup codes
- Use constant-time comparison - Prevent timing attacks
Practice Exercise
Review this authentication code and identify the security issues:
@app.route('/login', methods=['POST'])
def login():
user = User.query.filter_by(email=request.form['email']).first()
if user and user.password == hashlib.md5(request.form['password'].encode()).hexdigest():
session['user_id'] = user.id
session['is_admin'] = user.is_admin
return redirect('/dashboard')
return 'Invalid credentials', 401
@app.route('/api/token')
def get_token():
user_id = request.args.get('user_id')
token = jwt.encode(
{'user_id': user_id},
'secret',
algorithm='HS256'
)
return {'token': token}
Issues to find:
- MD5 for password hashing (weak, unsalted)
- Session not regenerated after login (fixation vulnerability)
- Storing
is_adminin session (can be tampered if session is client-side) - JWT secret is too weak ("secret")
- JWT has no expiration
- Token endpoint doesn't require authentication
- User ID from query parameter without validation
Part of: Secure Coding Practices
Updated: 1/24/2025
Found an issue?