8.9 KiB
8.9 KiB
Authentication Security Reference
Password Requirements
Strength Requirements
| Context | Minimum Length | Maximum Length |
|---|---|---|
| With MFA | 8 characters | At least 64 characters |
| Without MFA | 15 characters | At least 64 characters |
Composition Rules:
- Allow all printable characters including spaces and Unicode
- No mandatory complexity rules (uppercase, numbers, symbols)
- No periodic forced password changes
- Check against breached password databases (e.g., Have I Been Pwned)
- Implement password strength meters (e.g., zxcvbn)
Password Storage
Recommended Algorithms (in order of preference):
-
Argon2id (preferred)
Memory: minimum 19 MiB (19456 KB) Iterations: minimum 2 Parallelism: 1 -
scrypt
CPU/memory cost (N): 2^17 Block size (r): 8 Parallelization (p): 1 -
bcrypt (legacy systems)
Work factor: minimum 10 (ideally 12+) Maximum password length: 72 bytes -
PBKDF2 (FIPS-required environments)
Iterations: minimum 600,000 with HMAC-SHA-256
Never Use:
- MD5, SHA1, SHA256 without key stretching
- Plain hashing without salt
- Reversible encryption for passwords
Vulnerable Patterns
# VULNERABLE: MD5 hash
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
# VULNERABLE: SHA256 without salt/iterations
password_hash = hashlib.sha256(password.encode()).hexdigest()
# SAFE: bcrypt
import bcrypt
password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
# SAFE: Argon2
from argon2 import PasswordHasher
ph = PasswordHasher()
password_hash = ph.hash(password)
Error Messages
Generic Response Principle
Return identical error messages regardless of the specific failure reason.
Login Responses:
# WRONG: Reveals valid usernames
"User not found"
"Invalid password"
"Account locked"
# CORRECT: Generic message
"Login failed; Invalid user ID or password."
Password Recovery:
# WRONG: Reveals valid emails
"Email not found"
"Password reset email sent"
# CORRECT: Generic message
"If that email address is in our database, we will send you an email to reset your password."
Account Creation:
# WRONG: Reveals existing accounts
"Email already registered"
# CORRECT: Generic message
"A link to activate your account has been emailed to the address provided."
Brute Force Protection
Account Lockout
# Configuration
LOCKOUT_THRESHOLD = 5 # Failed attempts before lockout
OBSERVATION_WINDOW = 15 * 60 # 15 minutes
LOCKOUT_DURATION = 30 * 60 # 30 minutes
# Implementation
class LoginAttemptTracker:
def record_failed_attempt(self, account_id):
# Track by account, NOT by IP
# IP-based tracking allows bypassing via distributed attacks
pass
def is_locked(self, account_id):
# Check if account is locked
pass
def allow_password_reset_when_locked(self):
# Prevent lockout from becoming DoS
return True
Exponential Backoff
def get_lockout_duration(failed_attempts):
# Double duration with each lockout
base_duration = 60 # 1 minute
return base_duration * (2 ** (failed_attempts // LOCKOUT_THRESHOLD - 1))
Rate Limiting
# Per-IP rate limiting (defense in depth)
RATE_LIMIT = "10/minute"
# Per-account rate limiting
ACCOUNT_RATE_LIMIT = "5/minute"
Multi-Factor Authentication
MFA Effectiveness
Microsoft research indicates MFA blocks 99.9% of account compromises.
MFA Implementation Checklist
- Require MFA for all users (not just optional)
- Support multiple MFA methods (TOTP, WebAuthn, SMS as fallback)
- Implement MFA bypass codes for recovery (store securely)
- Require re-authentication before disabling MFA
- Log all MFA events
WebAuthn/FIDO2 (Preferred)
// Registration
const publicKeyCredential = await navigator.credentials.create({
publicKey: {
challenge: serverChallenge,
rp: { name: "Example Corp", id: "example.com" },
user: { id: userId, name: username, displayName: displayName },
pubKeyCredParams: [{ type: "public-key", alg: -7 }], // ES256
authenticatorSelection: { userVerification: "preferred" }
}
});
Benefits:
- Phishing-resistant (bound to origin)
- No shared secrets to steal
- Hardware-backed security
Session Security
Session ID Requirements
- Entropy: Minimum 64 bits of randomness
- Length: At least 16 characters (hex) or 128 bits
- Generation: Cryptographically secure random generator only
# VULNERABLE: Predictable session ID
session_id = str(user_id) + str(int(time.time()))
# SAFE: Cryptographically random
import secrets
session_id = secrets.token_hex(32) # 256 bits
Cookie Security Attributes
Set-Cookie: session_id=abc123;
Secure; # HTTPS only
HttpOnly; # No JavaScript access
SameSite=Lax; # CSRF protection
Path=/; # Scope
Max-Age=3600; # Expiration
Session Lifecycle
# VULNERABLE: Not regenerating session on login (Session Fixation)
def login(username, password):
user = authenticate(username, password)
session['user_id'] = user.id # Same session ID - attacker can pre-set it!
# SAFE: Regenerate session ID after authentication
def login(user, password):
if authenticate(user, password):
# CRITICAL: Generate new session ID to prevent fixation
session.regenerate()
session['user_id'] = user.id
# Regenerate after privilege changes
def elevate_privileges():
session.regenerate()
session['is_admin'] = True
# Proper logout - invalidate both server and client
def logout():
session.invalidate() # Server-side invalidation
response.delete_cookie('session_id')
Session Timeouts
| Type | Purpose | Typical Value |
|---|---|---|
| Idle Timeout | Inactive session | 15-30 minutes |
| Absolute Timeout | Maximum lifetime | 4-8 hours |
Concurrent Session Control
# Option 1: Allow only one session per user
def login(user):
invalidate_all_sessions(user.id)
return create_session(user)
# Option 2: Limit concurrent sessions
MAX_SESSIONS = 3
def login(user):
sessions = get_sessions_by_user(user.id)
if len(sessions) >= MAX_SESSIONS:
oldest = min(sessions, key=lambda s: s['created_at'])
invalidate_session(oldest['id'])
return create_session(user)
Re-authentication Requirements
Require fresh credentials before:
- Password changes
- Email address changes
- MFA configuration changes
- Sensitive financial transactions
- Account deletion
def requires_recent_auth(max_age=300): # 5 minutes
"""Decorator requiring recent authentication."""
def decorator(f):
def wrapper(*args, **kwargs):
last_auth = session.get('last_auth_time')
if not last_auth or time.time() - last_auth > max_age:
raise ReauthenticationRequired()
return f(*args, **kwargs)
return wrapper
return decorator
@requires_recent_auth(max_age=300)
def change_password(old_password, new_password):
pass
Email Address Changes
With MFA Enabled
- Verify current session authentication
- Request MFA verification
- Send notification to current email address
- Send confirmation link to new email address
- Require clicking link within time limit (e.g., 8 hours)
Without MFA
- Verify current session authentication
- Require current password verification
- Send notification to current email address
- Send confirmation link to both addresses
- Require confirmation from both within time limit
Grep Patterns for Detection
# Weak hashing
grep -rn "md5\|sha1\|sha256" --include="*.py" --include="*.js" | grep -i password
grep -rn "hashlib\\.md5\|hashlib\\.sha" --include="*.py"
# Predictable session IDs
grep -rn "uuid1\|time\\(\\).*session\|user.*id.*session" --include="*.py"
# Missing cookie security
grep -rn "Set-Cookie" --include="*.py" --include="*.js" | grep -v -i "secure\|httponly"
# Error message leakage
grep -rn "not found\|invalid password\|does not exist" --include="*.py" --include="*.js"
# Session handling
grep -rn "session\\.regenerate\|regenerate_id\|new_session" --include="*.py" --include="*.php"