05. Authentication Systems
05. Authentication Systems¶
Previous: 04. TLS/SSL and Public Key Infrastructure | Next: 06. Authorization and Access Control
Authentication is the process of verifying that a user or system is who they claim to be. It answers the question "Who are you?" and is the foundation upon which all access control decisions rest. A poorly implemented authentication system can render even the most sophisticated authorization and encryption useless. This lesson covers password-based authentication, multi-factor authentication, token-based systems, OAuth 2.0/OIDC, session management, and biometric approaches, with practical Python examples throughout.
Learning Objectives¶
- Implement secure password storage using salting and key stretching
- Understand and implement multi-factor authentication (TOTP, FIDO2/WebAuthn)
- Describe OAuth 2.0 and OpenID Connect authorization/authentication flows
- Manage sessions securely using cookies, tokens, and JWTs
- Identify and avoid common JWT pitfalls
- Design secure password reset flows
- Understand biometric authentication concepts and tradeoffs
1. Password-Based Authentication¶
1.1 The Password Problem¶
Passwords remain the most common authentication method despite their well-known weaknesses.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Password Authentication Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β User Server β
β ββββββββ ββββββββββββ β
β β Form βββββββββββββββββΆβ Receive β β
β β user β username + β username β β
β β pass β password β + pass β β
β ββββββββ ββββββ¬ββββββ β
β β β
β βΌ β
β ββββββββββββββββ β
β β Hash the β β
β β provided β β
β β password β β
β ββββββββ¬ββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββββ β
β β Compare hash to β β
β β stored hash in DB β β
β ββββββββββ¬ββββββββββββ β
β β β
β ββββββββ΄βββββββ β
β β β β
β Match? No Match? β
β β β β
β βΌ βΌ β
β ββββββββββ ββββββββββ β
β β Grant β β Deny β β
β β Access β β Access β β
β ββββββββββ ββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.2 Why Not Store Plaintext Passwords?¶
If an attacker gains access to your database (via SQL injection, backup theft, insider threat, etc.), plaintext passwords are immediately compromised. The principle of defense in depth requires that even a database breach does not directly expose user credentials.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NEVER DO THIS: β
β β
β users table: β
β ββββββββββββ¬βββββββββββββββββ β
β β username β password β β
β ββββββββββββΌβββββββββββββββββ€ β
β β alice β MyP@ssw0rd! β β Plaintext = catastrophe β
β β bob β hunter2 β β
β ββββββββββββ΄βββββββββββββββββ β
β β
β INSTEAD: β
β β
β users table: β
β ββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ β
β β username β password_hash β β
β ββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββββ€ β
β β alice β $2b$12$LJ3m4ys3Lk0aB... (bcrypt hash) β β
β β bob β $argon2id$v=19$m=65536... (argon2 hash) β β
β ββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1.3 Hashing, Salting, and Key Stretching¶
Hashing converts a password into a fixed-length string. But simple hashing (MD5, SHA-256) is vulnerable to rainbow tables and brute force.
Salting adds a unique random value to each password before hashing, defeating rainbow tables.
Key Stretching applies the hash function thousands or millions of times, making brute force computationally expensive.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Password Hashing Pipeline β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β "MyP@ssw0rd!" β
β β β
β βΌ β
β ββββββββββββββββ β
β β Generate β βββΆ salt = "x9Kp2mQ..." (random, unique) β
β β Random Salt β β
β ββββββββ¬ββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββ β
β β Concatenate β βββΆ "x9Kp2mQ..." + "MyP@ssw0rd!" β
β β salt + password β β
β ββββββββ¬ββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββββββ β
β β Key Stretching β βββΆ Apply hash 100,000+ iterations β
β β (bcrypt/argon2) β or memory-hard function β
β ββββββββ¬ββββββββββββ β
β β β
β βΌ β
β "$2b$12$x9Kp2mQ.../hashed_output" β
β (salt + hash stored together) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Recommended Algorithms (in order of preference):
| Algorithm | Type | Key Feature | Recommended Parameters |
|---|---|---|---|
| Argon2id | Memory-hard | GPU/ASIC resistant | m=65536, t=3, p=4 |
| bcrypt | CPU-hard | Widely supported | cost factor 12+ |
| scrypt | Memory-hard | Good alternative | N=2^15, r=8, p=1 |
| PBKDF2 | Iteration-based | NIST approved | 600,000+ iterations (SHA-256) |
1.4 Python Implementation: Password Hashing¶
"""
password_hashing.py - Secure password storage with bcrypt and argon2
"""
import bcrypt
import hashlib
import os
import secrets
# ==============================================================
# Method 1: bcrypt (most widely used)
# ==============================================================
def hash_password_bcrypt(password: str) -> str:
"""Hash a password using bcrypt with automatic salting."""
# bcrypt automatically generates a salt and includes it in the output
# The cost factor (rounds) controls computation time: 2^rounds iterations
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt(rounds=12) # 2^12 = 4096 iterations
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def verify_password_bcrypt(password: str, stored_hash: str) -> bool:
"""Verify a password against a bcrypt hash."""
password_bytes = password.encode('utf-8')
stored_bytes = stored_hash.encode('utf-8')
return bcrypt.checkpw(password_bytes, stored_bytes)
# ==============================================================
# Method 2: Argon2 (recommended by OWASP)
# ==============================================================
# pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
def hash_password_argon2(password: str) -> str:
"""Hash a password using Argon2id."""
ph = PasswordHasher(
time_cost=3, # Number of iterations
memory_cost=65536, # 64 MB of memory
parallelism=4, # Number of parallel threads
hash_len=32, # Length of the hash output
salt_len=16 # Length of the random salt
)
return ph.hash(password)
def verify_password_argon2(password: str, stored_hash: str) -> bool:
"""Verify a password against an Argon2 hash."""
ph = PasswordHasher()
try:
return ph.verify(stored_hash, password)
except VerifyMismatchError:
return False
# ==============================================================
# Method 3: PBKDF2 (built into Python, no external deps)
# ==============================================================
def hash_password_pbkdf2(password: str) -> str:
"""Hash a password using PBKDF2-HMAC-SHA256."""
salt = os.urandom(32) # 32-byte random salt
iterations = 600_000 # OWASP recommended minimum for SHA-256
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations,
dklen=32
)
# Store salt + iterations + hash together
# Format: iterations$salt_hex$hash_hex
return f"{iterations}${salt.hex()}${key.hex()}"
def verify_password_pbkdf2(password: str, stored: str) -> bool:
"""Verify a password against a PBKDF2 hash."""
iterations_str, salt_hex, hash_hex = stored.split('$')
iterations = int(iterations_str)
salt = bytes.fromhex(salt_hex)
stored_key = bytes.fromhex(hash_hex)
key = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt,
iterations,
dklen=32
)
# Use constant-time comparison to prevent timing attacks
return secrets.compare_digest(key, stored_key)
# ==============================================================
# Demo
# ==============================================================
if __name__ == "__main__":
test_password = "MySecureP@ssw0rd!"
# bcrypt
print("=== bcrypt ===")
hashed = hash_password_bcrypt(test_password)
print(f"Hash: {hashed}")
print(f"Verify (correct): {verify_password_bcrypt(test_password, hashed)}")
print(f"Verify (wrong): {verify_password_bcrypt('wrong', hashed)}")
# Argon2
print("\n=== Argon2id ===")
hashed = hash_password_argon2(test_password)
print(f"Hash: {hashed}")
print(f"Verify (correct): {verify_password_argon2(test_password, hashed)}")
print(f"Verify (wrong): {verify_password_argon2('wrong', hashed)}")
# PBKDF2
print("\n=== PBKDF2 ===")
hashed = hash_password_pbkdf2(test_password)
print(f"Hash: {hashed}")
print(f"Verify (correct): {verify_password_pbkdf2(test_password, hashed)}")
print(f"Verify (wrong): {verify_password_pbkdf2('wrong', hashed)}")
1.5 Password Policies¶
Strong passwords alone are insufficient. A comprehensive password policy includes:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Modern Password Policy (NIST SP 800-63B) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β DO: β
β β Minimum 8 characters (12+ recommended) β
β β Maximum 64+ characters allowed β
β β Allow all printable ASCII + Unicode characters β
β β Check against breached password lists (haveibeenpwned.com) β
β β Check against common passwords (password, 123456, etc.) β
β β Allow paste into password fields (for password managers) β
β β Show password strength meter β
β β
β DON'T: β
β β Force arbitrary complexity rules (uppercase + number + ...) β
β β Force periodic password rotation (unless breach suspected) β
β β Use password hints or knowledge-based questions β
β β Truncate passwords silently β
β β Use SMS for password recovery (SIM swapping attacks) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"""
password_policy.py - Modern password validation per NIST guidelines
"""
import re
import hashlib
import requests
from typing import Tuple, List
# Common passwords list (top 20 - in practice, use a much larger list)
COMMON_PASSWORDS = {
"password", "123456", "123456789", "12345678", "12345",
"1234567", "qwerty", "abc123", "password1", "111111",
"iloveyou", "1234567890", "123123", "admin", "letmein",
"welcome", "monkey", "dragon", "master", "000000",
}
def check_password_strength(password: str) -> Tuple[bool, List[str]]:
"""
Validate password against modern security guidelines.
Returns (is_valid, list_of_issues).
"""
issues = []
# Length check (NIST minimum: 8, recommended: 12+)
if len(password) < 8:
issues.append("Password must be at least 8 characters long")
elif len(password) < 12:
issues.append("Warning: 12+ characters recommended for better security")
# Common password check
if password.lower() in COMMON_PASSWORDS:
issues.append("This is a commonly used password")
# Repetitive pattern check
if re.match(r'^(.)\1+$', password):
issues.append("Password cannot be a single repeated character")
# Sequential pattern check
if re.search(r'(012|123|234|345|456|567|678|789|890)', password):
issues.append("Warning: Contains sequential number pattern")
# Context-specific check (would include username, email, etc.)
# In production, also check against the user's personal info
is_valid = not any(
not issue.startswith("Warning") for issue in issues
)
return is_valid, issues
def check_breached_password(password: str) -> bool:
"""
Check if password appears in known breaches using the
Have I Been Pwned API (k-anonymity model - only first 5
chars of SHA-1 hash are sent).
"""
sha1 = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
prefix = sha1[:5]
suffix = sha1[5:]
try:
response = requests.get(
f"https://api.pwnedpasswords.com/range/{prefix}",
timeout=5
)
response.raise_for_status()
# Response contains lines of "SUFFIX:COUNT"
for line in response.text.splitlines():
hash_suffix, count = line.split(':')
if hash_suffix == suffix:
return True # Password has been breached
return False # Password not found in breaches
except requests.RequestException:
# If API is unavailable, fail open (but log the error)
return False
if __name__ == "__main__":
test_passwords = [
"short",
"password",
"MyStr0ng&Secure!Pass",
"aaaaaaaaaaaa",
"12345678",
]
for pwd in test_passwords:
is_valid, issues = check_password_strength(pwd)
status = "PASS" if is_valid else "FAIL"
print(f"\n[{status}] '{pwd}'")
for issue in issues:
print(f" - {issue}")
2. Multi-Factor Authentication (MFA)¶
2.1 Authentication Factors¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authentication Factors β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Factor 1: Something You KNOW β
β βββ Password β
β βββ PIN β
β βββ Security questions (discouraged) β
β β
β Factor 2: Something You HAVE β
β βββ Smartphone (TOTP app) β
β βββ Hardware security key (YubiKey, Titan) β
β βββ Smart card β
β βββ SMS (weak - SIM swapping risk) β
β β
β Factor 3: Something You ARE β
β βββ Fingerprint β
β βββ Face recognition β
β βββ Iris scan β
β βββ Voice recognition β
β β
β MFA = Combining 2+ different factors β
β (password + TOTP = 2FA, but two passwords β 2FA) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
2.2 TOTP (Time-Based One-Time Password)¶
TOTP generates a short-lived code based on a shared secret and the current time. Defined in RFC 6238.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TOTP Algorithm β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Setup (one-time): β
β 1. Server generates random secret key (base32 encoded) β
β 2. Server shares secret with user via QR code β
β 3. User scans QR code with authenticator app β
β β
β Verification (each login): β
β β
β Shared Secret Current Time β
β β β β
β βΌ βΌ β
β βββββββββββ ββββββββββββββββ β
β β Secret β β T = floor β β
β β Key β β (time / 30) β β
β ββββββ¬βββββ ββββββββ¬ββββββββ β
β β β β
β ββββββββββ¬ββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββ β
β β HMAC-SHA1 β β
β β (secret, T) β β
β ββββββββ¬βββββββ β
β β β
β βΌ β
β βββββββββββββββ β
β β Truncate to β β
β β 6 digits β β
β ββββββββ¬βββββββ β
β β β
β βΌ β
β "482916" (valid for 30 seconds) β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"""
totp_example.py - TOTP implementation using pyotp
pip install pyotp qrcode[pil]
"""
import pyotp
import qrcode
import time
import io
class TOTPManager:
"""Manage TOTP-based two-factor authentication."""
def __init__(self):
self.secrets = {} # In production, store in encrypted database
def enroll_user(self, username: str, issuer: str = "MyApp") -> str:
"""
Generate a TOTP secret for a new user.
Returns the provisioning URI for QR code generation.
"""
# Generate a random base32 secret (160 bits)
secret = pyotp.random_base32()
self.secrets[username] = secret
# Generate provisioning URI for authenticator apps
totp = pyotp.TOTP(secret)
uri = totp.provisioning_uri(
name=username,
issuer_name=issuer
)
return uri, secret
def generate_qr_code(self, uri: str, filename: str = "totp_qr.png"):
"""Generate a QR code image from the provisioning URI."""
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(uri)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(filename)
print(f"QR code saved to {filename}")
def verify_totp(self, username: str, code: str) -> bool:
"""
Verify a TOTP code for the given user.
Allows 1 period of clock drift (Β±30 seconds).
"""
if username not in self.secrets:
return False
secret = self.secrets[username]
totp = pyotp.TOTP(secret)
# valid_window=1 allows codes from t-1 and t+1 periods
return totp.verify(code, valid_window=1)
def get_current_code(self, username: str) -> str:
"""Get the current TOTP code (for testing only)."""
if username not in self.secrets:
return None
secret = self.secrets[username]
totp = pyotp.TOTP(secret)
return totp.now()
# Backup codes for account recovery
def generate_backup_codes(count: int = 10) -> list:
"""
Generate one-time-use backup codes.
Each code is 8 characters, alphanumeric.
"""
import secrets
codes = []
for _ in range(count):
code = secrets.token_hex(4).upper() # 8 hex characters
# Format as XXXX-XXXX for readability
formatted = f"{code[:4]}-{code[4:]}"
codes.append(formatted)
return codes
if __name__ == "__main__":
manager = TOTPManager()
# Enroll user
uri, secret = manager.enroll_user("alice@example.com")
print(f"Secret: {secret}")
print(f"URI: {uri}")
# Generate current code
current_code = manager.get_current_code("alice@example.com")
print(f"\nCurrent TOTP code: {current_code}")
# Verify
print(f"Verification: {manager.verify_totp('alice@example.com', current_code)}")
print(f"Wrong code: {manager.verify_totp('alice@example.com', '000000')}")
# Generate backup codes
print("\nBackup Codes:")
for code in generate_backup_codes():
print(f" {code}")
2.3 FIDO2 / WebAuthn¶
FIDO2 (Fast Identity Online) and its web component WebAuthn represent the strongest form of authentication available today. They use public-key cryptography and are phishing-resistant.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WebAuthn Registration Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Browser (Client) Server (Relying Party) β
β β β β
β β 1. Request challenge β β
β β βββββββββββββββββββββββΆ β β
β β β β
β β 2. Challenge + β β
β β RP info + user info β β
β β βββββββββββββββββββββββ β β
β β β β
β ββββββ΄βββββ β β
β β Browser β β β
β β prompts β β β
β β user to β β β
β β touch β β β
β β key or β β β
β β use β β β
β β biometr.β β β
β ββββββ¬βββββ β β
β β β β
β ββββββ΄βββββββββββββββ β β
β β Authenticator β β β
β β generates new β β β
β β key pair: β β β
β β - Private key β β β
β β (stored in key) β β β
β β - Public key β β β
β β (sent to server)β β β
β ββββββ¬βββββββββββββββ β β
β β β β
β β 3. Public key + β β
β β signed challenge β β
β β βββββββββββββββββββββββΆ β β
β β β β
β β βββββββ΄ββββββ β
β β β Verify β β
β β β signature β β
β β β Store β β
β β β public keyβ β
β β βββββββ¬ββββββ β
β β β β
β β 4. Registration OK β β
β β βββββββββββββββββββββββ β β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key advantages of WebAuthn:
| Feature | Passwords | TOTP | WebAuthn/FIDO2 |
|---|---|---|---|
| Phishing resistant | No | No | Yes |
| No shared secrets on server | No | No | Yes (public key only) |
| User effort | High (memorize) | Medium (copy code) | Low (touch/biometric) |
| Replay attacks | Vulnerable | Time-limited | Not possible |
| Breach impact | High | Medium | Minimal |
3. OAuth 2.0 and OpenID Connect¶
3.1 OAuth 2.0 Overview¶
OAuth 2.0 is an authorization framework (not authentication). It allows a third-party application to access resources on behalf of a user without sharing the user's credentials.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OAuth 2.0 Roles β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Resource Owner = The user who owns the data β
β Client = The application requesting access β
β Authorization = The server that authenticates the user β
β Server and issues tokens (e.g., Google, GitHub) β
β Resource Server = The API server holding protected resources β
β β
β Example: β
β "MyApp wants to access your Google Calendar" β
β β
β Resource Owner = You (the Google user) β
β Client = MyApp β
β Authorization = accounts.google.com β
β Server β
β Resource Server = calendar.googleapis.com β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3.2 Authorization Code Flow (Most Common)¶
This is the recommended flow for server-side web applications.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OAuth 2.0 Authorization Code Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β User Client App Auth Server Resource Server β
β β β β β β
β β 1. Click β β β β
β β "Login w/ β β β β
β β Google" β β β β
β βββββββββββββββΆβ β β β
β β β β β β
β β β 2. Redirect β β β
β β β to auth URL β β β
β ββββββββββββββββ β β β
β β β β β β
β β 3. User logs in and β β β
β β consents to permissions β β β
β βββββββββββββββββββββββββββββββΆβ β β
β β β β β β
β β 4. Redirect back with β β β
β β authorization code β β β
β ββββββββββββββββββββββββββββββββ β β
β β β β β β
β βββββββββββββββΆβ β β β
β β (code) β β β β
β β β 5. Exchange β β β
β β β code for β β β
β β β tokens β β β
β β βββββββββββββββββΆβ β β
β β β β β β
β β β 6. Access + β β β
β β β Refresh tokensβ β β
β β ββββββββββββββββββ β β
β β β β β β
β β β 7. API request with access token β β
β β ββββββββββββββββββββββββββββββββββββΆβ β
β β β β β β
β β β 8. Protected resource β β
β β βββββββββββββββββββββββββββββββββββββ β
β β β β β β
β β 9. Response β β β β
β ββββββββββββββββ β β β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3.3 Authorization Code Flow with PKCE¶
PKCE (Proof Key for Code Exchange) is required for public clients (SPAs, mobile apps) and recommended for all clients.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PKCE Extension β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Before redirect (step 2): β
β β
β 1. Client generates random "code_verifier" β
β code_verifier = random_string(43-128 chars) β
β β
β 2. Client computes "code_challenge" β
β code_challenge = BASE64URL(SHA256(code_verifier)) β
β β
β 3. Client sends code_challenge in authorization request β
β GET /authorize? β
β response_type=code& β
β client_id=...& β
β code_challenge=...& β
β code_challenge_method=S256 β
β β
β At token exchange (step 5): β
β β
β 4. Client sends code_verifier with token request β
β POST /token β
β grant_type=authorization_code& β
β code=...& β
β code_verifier=... β
β β
β 5. Server verifies: β
β BASE64URL(SHA256(code_verifier)) == stored code_challenge β
β β
β Why? Prevents authorization code interception attacks. β
β An attacker who steals the code cannot exchange it β
β without the code_verifier. β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3.4 OpenID Connect (OIDC)¶
OpenID Connect is an authentication layer built on top of OAuth 2.0. While OAuth 2.0 provides authorization ("this app can access your calendar"), OIDC provides authentication ("this user is alice@example.com").
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β OAuth 2.0 vs OpenID Connect β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β OAuth 2.0: β
β - Purpose: Authorization (access delegation) β
β - Token: Access Token (opaque or JWT) β
β - Question answered: "What can this app do?" β
β β
β OpenID Connect (OIDC): β
β - Purpose: Authentication (identity verification) β
β - Token: ID Token (always JWT) + Access Token β
β - Question answered: "Who is this user?" β
β - Adds: UserInfo endpoint, standard claims (sub, email, name) β
β - Scope: "openid" (required), "profile", "email" β
β β
β OIDC builds ON TOP of OAuth 2.0: β
β βββββββββββββββββββββββββββββββββββ β
β β OpenID Connect β β Authentication β
β β ββββββββββββββββββββββββββββ β β
β β β OAuth 2.0 β β β Authorization β
β β β βββββββββββββββββββββ β β β
β β β β HTTP/TLS β β β β Transport β
β β β βββββββββββββββββββββ β β β
β β ββββββββββββββββββββββββββββ β β
β βββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
3.5 Python Example: OAuth 2.0 Client¶
"""
oauth_client.py - OAuth 2.0 Authorization Code Flow with PKCE
Using the requests-oauthlib library
pip install requests-oauthlib
"""
import hashlib
import base64
import secrets
import os
from urllib.parse import urlencode, urlparse, parse_qs
from flask import Flask, redirect, request, session, jsonify
import requests
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
# OAuth 2.0 Configuration (example with GitHub)
OAUTH_CONFIG = {
"client_id": os.environ.get("OAUTH_CLIENT_ID", "your-client-id"),
"client_secret": os.environ.get("OAUTH_CLIENT_SECRET", "your-secret"),
"authorize_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"userinfo_url": "https://api.github.com/user",
"redirect_uri": "http://localhost:5000/callback",
"scope": "read:user user:email",
}
def generate_pkce_pair():
"""Generate PKCE code_verifier and code_challenge."""
# code_verifier: 43-128 chars, unreserved URI characters
code_verifier = base64.urlsafe_b64encode(
secrets.token_bytes(32)
).rstrip(b'=').decode('ascii')
# code_challenge: BASE64URL(SHA256(code_verifier))
digest = hashlib.sha256(code_verifier.encode('ascii')).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
return code_verifier, code_challenge
@app.route("/login")
def login():
"""Initiate OAuth 2.0 Authorization Code Flow."""
# Generate PKCE pair
code_verifier, code_challenge = generate_pkce_pair()
# Generate state for CSRF protection
state = secrets.token_urlsafe(32)
# Store in session
session["oauth_state"] = state
session["code_verifier"] = code_verifier
# Build authorization URL
params = {
"client_id": OAUTH_CONFIG["client_id"],
"redirect_uri": OAUTH_CONFIG["redirect_uri"],
"scope": OAUTH_CONFIG["scope"],
"response_type": "code",
"state": state,
"code_challenge": code_challenge,
"code_challenge_method": "S256",
}
auth_url = f"{OAUTH_CONFIG['authorize_url']}?{urlencode(params)}"
return redirect(auth_url)
@app.route("/callback")
def callback():
"""Handle OAuth 2.0 callback with authorization code."""
# Verify state to prevent CSRF
if request.args.get("state") != session.get("oauth_state"):
return "State mismatch - possible CSRF attack", 403
# Check for errors
if "error" in request.args:
return f"OAuth error: {request.args['error']}", 400
# Exchange authorization code for tokens
code = request.args.get("code")
token_response = requests.post(
OAUTH_CONFIG["token_url"],
data={
"client_id": OAUTH_CONFIG["client_id"],
"client_secret": OAUTH_CONFIG["client_secret"],
"code": code,
"redirect_uri": OAUTH_CONFIG["redirect_uri"],
"grant_type": "authorization_code",
"code_verifier": session.get("code_verifier"),
},
headers={"Accept": "application/json"},
)
tokens = token_response.json()
access_token = tokens.get("access_token")
if not access_token:
return "Failed to obtain access token", 400
# Fetch user info
user_response = requests.get(
OAUTH_CONFIG["userinfo_url"],
headers={"Authorization": f"Bearer {access_token}"},
)
user_info = user_response.json()
# Store user in session
session["user"] = {
"id": user_info.get("id"),
"login": user_info.get("login"),
"name": user_info.get("name"),
"email": user_info.get("email"),
}
# Clean up OAuth state
session.pop("oauth_state", None)
session.pop("code_verifier", None)
return redirect("/profile")
@app.route("/profile")
def profile():
"""Display user profile (protected route)."""
user = session.get("user")
if not user:
return redirect("/login")
return jsonify(user)
@app.route("/logout")
def logout():
"""Clear session and log out."""
session.clear()
return redirect("/")
4. Session Management¶
4.1 Server-Side Sessions (Cookie-Based)¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Server-Side Session Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Browser Server β
β β β β
β β 1. POST /login β β
β β (username + password) β β
β βββββββββββββββββββββββββββββββββΆβ β
β β β β
β β βββββββ΄ββββββ β
β β β Validate β β
β β β Create β β
β β β session β β
β β β ID=abc123 β β
β β β Store in β β
β β β Redis/DB β β
β β βββββββ¬ββββββ β
β β β β
β β 2. Set-Cookie: β β
β β session_id=abc123; β β
β β HttpOnly; Secure; β β
β β SameSite=Lax β β
β ββββββββββββββββββββββββββββββββββ β
β β β β
β β 3. GET /dashboard β β
β β Cookie: session_id=abc123 β β
β βββββββββββββββββββββββββββββββββΆβ β
β β βββββββ΄ββββββ β
β β β Look up β β
β β β session β β
β β β in store β β
β β βββββββ¬ββββββ β
β β 4. Response with user data β β
β ββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
4.2 Secure Cookie Attributes¶
Set-Cookie: session_id=abc123;
HttpOnly; β Cannot be accessed by JavaScript (XSS protection)
Secure; β Only sent over HTTPS
SameSite=Lax; β CSRF protection (not sent on cross-site POST)
Path=/; β Cookie scope
Max-Age=3600; β Expires in 1 hour
Domain=.app.com β Sent to subdomains too
| Attribute | Purpose | Recommended Value |
|---|---|---|
HttpOnly |
Prevent XSS from reading cookie | Always set |
Secure |
HTTPS only | Always set in production |
SameSite |
CSRF protection | Lax (or Strict for sensitive actions) |
Max-Age |
Session duration | 1-24 hours depending on sensitivity |
Path |
URL scope | / or specific path |
4.3 Session Security Best Practices¶
"""
session_security.py - Secure session management with Flask
"""
from flask import Flask, session, request, redirect, url_for
from datetime import timedelta
import secrets
import time
app = Flask(__name__)
# Session configuration
app.config.update(
SECRET_KEY=secrets.token_hex(32),
SESSION_COOKIE_HTTPONLY=True, # Prevent JavaScript access
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_SAMESITE='Lax', # CSRF protection
PERMANENT_SESSION_LIFETIME=timedelta(hours=1), # Session timeout
SESSION_COOKIE_NAME='__Host-session', # __Host- prefix forces Secure+Path=/
)
@app.before_request
def check_session_security():
"""Middleware to enforce session security policies."""
if 'user_id' not in session:
return # Not logged in, skip checks
# 1. Session timeout (absolute)
created_at = session.get('created_at', 0)
if time.time() - created_at > 3600: # 1 hour absolute timeout
session.clear()
return redirect(url_for('login'))
# 2. Idle timeout
last_active = session.get('last_active', 0)
if time.time() - last_active > 900: # 15 min idle timeout
session.clear()
return redirect(url_for('login'))
# 3. Update last active time
session['last_active'] = time.time()
# 4. IP binding (optional - can cause issues with mobile users)
if session.get('ip_address') != request.remote_addr:
# Log suspicious activity
app.logger.warning(
f"IP change detected for user {session.get('user_id')}: "
f"{session.get('ip_address')} -> {request.remote_addr}"
)
def regenerate_session(user_id: int):
"""
Regenerate session ID after authentication state change.
Prevents session fixation attacks.
"""
# Preserve necessary data
old_data = dict(session)
# Clear old session
session.clear()
# Create new session with fresh ID
session['user_id'] = user_id
session['created_at'] = time.time()
session['last_active'] = time.time()
session['ip_address'] = request.remote_addr
session.permanent = True # Use PERMANENT_SESSION_LIFETIME
# Note: Flask automatically generates a new session ID
# when the session is modified after being cleared
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
# Validate credentials (simplified)
user = authenticate(username, password)
if user:
# IMPORTANT: Regenerate session after login
regenerate_session(user.id)
return redirect(url_for('dashboard'))
return "Invalid credentials", 401
@app.route('/logout')
def logout():
"""Properly destroy the session."""
session.clear()
response = redirect(url_for('login'))
# Explicitly expire the cookie
response.delete_cookie('__Host-session')
return response
5. JSON Web Tokens (JWT)¶
5.1 JWT Structure¶
A JWT consists of three Base64URL-encoded parts separated by dots.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JWT Structure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. β
β eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4iLCJpYXQiOjE2M. β
β SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c β
β β
β ββββββββββββββββββββ β
β β HEADER β β Algorithm + token type β
β β { β β
β β "alg": "HS256"β HMAC SHA-256 β
β β "typ": "JWT" β JSON Web Token β
β β } β β
β ββββββββββββββββββββ β
β . β
β ββββββββββββββββββββ β
β β PAYLOAD β β Claims (data) β
β β { β β
β β "sub": "1234" β Subject (user ID) β
β β "name": "John"β Custom claim β
β β "iat": 163... β Issued at β
β β "exp": 163... β Expiration β
β β "iss": "myapp"β Issuer β
β β "aud": "api" β Audience β
β β } β β
β ββββββββββββββββββββ β
β . β
β ββββββββββββββββββββ β
β β SIGNATURE β β Integrity verification β
β β β β
β β HMACSHA256( β β
β β base64url( β β
β β header) + β β
β β "." + β β
β β base64url( β β
β β payload), β β
β β secret β β
β β ) β β
β ββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
5.2 JWT Signing Algorithms¶
| Algorithm | Type | Key | Use Case |
|---|---|---|---|
| HS256 | Symmetric | Shared secret | Single service (same key signs and verifies) |
| RS256 | Asymmetric | RSA key pair | Microservices (private key signs, public key verifies) |
| ES256 | Asymmetric | ECDSA key pair | Modern alternative to RS256 (smaller keys) |
| EdDSA | Asymmetric | Ed25519 pair | Best performance, smallest keys |
| none | None | None | NEVER USE - critical vulnerability |
5.3 Python JWT Implementation¶
"""
jwt_auth.py - JWT creation, verification, and common patterns
pip install PyJWT cryptography
"""
import jwt
import time
import secrets
from datetime import datetime, timedelta, timezone
from typing import Optional, Dict, Any
# ==============================================================
# Symmetric (HS256) - For single-service applications
# ==============================================================
class JWTManagerSymmetric:
"""JWT manager using HMAC-SHA256 (symmetric key)."""
def __init__(self, secret_key: str = None):
# In production, load from environment variable
self.secret_key = secret_key or secrets.token_hex(32)
self.algorithm = "HS256"
def create_access_token(
self,
user_id: str,
roles: list = None,
expires_minutes: int = 15
) -> str:
"""Create a short-lived access token."""
now = datetime.now(timezone.utc)
payload = {
"sub": user_id, # Subject (user identifier)
"iat": now, # Issued at
"exp": now + timedelta(minutes=expires_minutes), # Expiration
"iss": "myapp", # Issuer
"aud": "myapp-api", # Audience
"type": "access", # Token type
"roles": roles or [], # User roles
"jti": secrets.token_hex(16), # Unique token ID (for revocation)
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def create_refresh_token(
self,
user_id: str,
expires_days: int = 30
) -> str:
"""Create a long-lived refresh token."""
now = datetime.now(timezone.utc)
payload = {
"sub": user_id,
"iat": now,
"exp": now + timedelta(days=expires_days),
"iss": "myapp",
"type": "refresh",
"jti": secrets.token_hex(16),
}
return jwt.encode(payload, self.secret_key, algorithm=self.algorithm)
def verify_token(self, token: str, expected_type: str = "access") -> Dict:
"""
Verify and decode a JWT token.
Raises jwt.InvalidTokenError on failure.
"""
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm], # IMPORTANT: always specify!
issuer="myapp",
audience="myapp-api",
options={
"require": ["exp", "iat", "sub", "iss"],
"verify_exp": True,
"verify_iat": True,
"verify_iss": True,
}
)
# Verify token type
if payload.get("type") != expected_type:
raise jwt.InvalidTokenError(
f"Expected {expected_type} token, got {payload.get('type')}"
)
return payload
except jwt.ExpiredSignatureError:
raise jwt.InvalidTokenError("Token has expired")
except jwt.InvalidAudienceError:
raise jwt.InvalidTokenError("Invalid audience")
except jwt.InvalidIssuerError:
raise jwt.InvalidTokenError("Invalid issuer")
except jwt.DecodeError:
raise jwt.InvalidTokenError("Token decode failed")
# ==============================================================
# Asymmetric (RS256) - For microservices
# ==============================================================
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
class JWTManagerAsymmetric:
"""JWT manager using RSA-SHA256 (asymmetric keys)."""
def __init__(self, private_key_pem: str = None, public_key_pem: str = None):
if private_key_pem and public_key_pem:
self.private_key = serialization.load_pem_private_key(
private_key_pem.encode(), password=None
)
self.public_key = serialization.load_pem_public_key(
public_key_pem.encode()
)
else:
# Generate key pair for demo
self.private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
self.public_key = self.private_key.public_key()
self.algorithm = "RS256"
def create_token(self, payload: dict) -> str:
"""Sign a token with the private key."""
return jwt.encode(payload, self.private_key, algorithm=self.algorithm)
def verify_token(self, token: str) -> dict:
"""Verify a token with the public key."""
return jwt.decode(
token,
self.public_key,
algorithms=[self.algorithm],
)
def get_public_key_pem(self) -> str:
"""Export public key (share with other services)."""
return self.public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
).decode()
# ==============================================================
# Token Refresh Pattern
# ==============================================================
class TokenService:
"""Complete token service with refresh flow."""
def __init__(self):
self.jwt_manager = JWTManagerSymmetric()
# In production, use Redis or a database
self.revoked_tokens = set()
def login(self, user_id: str, roles: list) -> Dict[str, str]:
"""Issue access and refresh tokens upon login."""
return {
"access_token": self.jwt_manager.create_access_token(
user_id, roles, expires_minutes=15
),
"refresh_token": self.jwt_manager.create_refresh_token(
user_id, expires_days=30
),
"token_type": "Bearer",
"expires_in": 900, # 15 minutes in seconds
}
def refresh(self, refresh_token: str) -> Dict[str, str]:
"""Use refresh token to get a new access token."""
# Verify refresh token
payload = self.jwt_manager.verify_token(
refresh_token, expected_type="refresh"
)
# Check if token has been revoked
jti = payload.get("jti")
if jti in self.revoked_tokens:
raise jwt.InvalidTokenError("Token has been revoked")
# Revoke old refresh token (rotation)
self.revoked_tokens.add(jti)
# Issue new token pair
user_id = payload["sub"]
return self.login(user_id, roles=[]) # Re-fetch roles from DB
def revoke(self, token: str):
"""Revoke a token (logout)."""
try:
payload = jwt.decode(
token,
self.jwt_manager.secret_key,
algorithms=["HS256"],
options={"verify_exp": False} # Allow revoking expired tokens
)
jti = payload.get("jti")
if jti:
self.revoked_tokens.add(jti)
except jwt.DecodeError:
pass # Invalid token, nothing to revoke
# ==============================================================
# Demo
# ==============================================================
if __name__ == "__main__":
print("=== Symmetric JWT (HS256) ===")
manager = JWTManagerSymmetric()
token = manager.create_access_token("user123", roles=["admin", "editor"])
print(f"Token: {token[:50]}...")
payload = manager.verify_token(token)
print(f"Payload: {payload}")
print("\n=== Token Service ===")
service = TokenService()
tokens = service.login("user123", ["admin"])
print(f"Access: {tokens['access_token'][:50]}...")
print(f"Refresh: {tokens['refresh_token'][:50]}...")
# Refresh
new_tokens = service.refresh(tokens["refresh_token"])
print(f"New access: {new_tokens['access_token'][:50]}...")
print("\n=== Asymmetric JWT (RS256) ===")
asym_manager = JWTManagerAsymmetric()
token = asym_manager.create_token({
"sub": "user123",
"exp": datetime.now(timezone.utc) + timedelta(hours=1),
})
print(f"Token: {token[:50]}...")
payload = asym_manager.verify_token(token)
print(f"Payload: {payload}")
print(f"\nPublic key (share with other services):")
print(asym_manager.get_public_key_pem()[:100] + "...")
5.4 Common JWT Pitfalls¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JWT Security Pitfalls β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. Algorithm "none" Attack β
β βββββββββββββββββββββ β
β Attacker changes header to {"alg": "none"} and removes β
β signature. Server accepts unsigned token. β
β β
β FIX: Always specify allowed algorithms: β
β jwt.decode(token, key, algorithms=["HS256"]) β
β NEVER use algorithms=["none"] or accept any algorithm β
β β
β 2. Algorithm Confusion (RS256 β HS256) β
β ββββββββββββββββββββββββββββββββββ β
β Server uses RS256 (asymmetric). Attacker changes to HS256 β
β and signs with the PUBLIC key. Server verifies using the β
β same public key as HMAC secret β valid! β
β β
β FIX: Explicitly specify expected algorithm, not just "any" β
β Use separate keys for symmetric/asymmetric β
β β
β 3. No Expiration β
β βββββββββββββ β
β Token without "exp" claim lives forever. β
β β
β FIX: Always set exp. Use short-lived access tokens (15 min) β
β with longer refresh tokens. β
β β
β 4. Sensitive Data in Payload β
β βββββββββββββββββββββββββ β
β JWT payload is Base64-encoded, NOT encrypted. β
β Anyone can decode and read it. β
β β
β FIX: Never put passwords, PII, or secrets in JWT payload β
β Use JWE (JSON Web Encryption) if payload must be private β
β β
β 5. Token Not Revocable β
β ββββββββββββββββββ β
β JWTs are stateless - once issued, they remain valid β
β until expiration. Logout doesn't invalidate the token. β
β β
β FIX: Use short expiry + token blocklist (Redis) for logout β
β Or use "jti" claim and track revoked token IDs β
β β
β 6. Storing JWT in localStorage β
β βββββββββββββββββββββββββ β
β localStorage is accessible by any JavaScript on the page, β
β making it vulnerable to XSS. β
β β
β FIX: Store in HttpOnly cookie (immune to XSS) β
β Or use in-memory storage + refresh token in HttpOnly cookie β
β β
β 7. Weak Secret Key β
β ββββββββββββββββ β
β Using short or guessable secret for HMAC signing. β
β Attackers can brute-force the key. β
β β
β FIX: Use at least 256 bits of entropy: β
β secret = secrets.token_hex(32) # 256 bits β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6. Password Reset Flows¶
6.1 Secure Password Reset Design¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Secure Password Reset Flow β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β User Server Email β
β β β β β
β β 1. "Forgot password" β β β
β β (enter email) β β β
β βββββββββββββββββββββββββββΆβ β β
β β β β β
β β βββββββ΄ββββββ β β
β β β Generate β β β
β β β random β β β
β β β token β β β
β β β Store β β β
β β β hash(tkn) β β β
β β β + expiry β β β
β β βββββββ¬ββββββ β β
β β β β β
β β 2. "Check your email" β 3. Send reset link β β
β β (SAME response whether β with token β β
β β email exists or not!) βββββββββββββββββββββββββββΆβ β
β ββββββββββββββββββββββββββββ β β
β β β β β
β β 4. Click link in email β β β
β β /reset?token=abc123 β β β
β βββββββββββββββββββββββββββΆβ β β
β β β β β
β β 5. New password form β β β
β ββββββββββββββββββββββββββββ β β
β β β β β
β β 6. Submit new password β β β
β βββββββββββββββββββββββββββΆβ β β
β β βββββββ΄ββββββ β β
β β β Verify β β β
β β β token β β β
β β β Update β β β
β β β password β β β
β β β Invalidateβ β β
β β β token β β β
β β β Invalidateβ β β
β β β sessions β β β
β β βββββββ¬ββββββ β β
β β β β β
β β 7. "Password updated" β β β
β ββββββββββββββββββββββββββββ β β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
6.2 Implementation¶
"""
password_reset.py - Secure password reset implementation
"""
import secrets
import hashlib
import time
from dataclasses import dataclass
from typing import Optional
@dataclass
class ResetToken:
token_hash: str
user_id: int
created_at: float
expires_at: float
used: bool = False
class PasswordResetService:
"""Secure password reset token management."""
TOKEN_EXPIRY_MINUTES = 30
MAX_REQUESTS_PER_HOUR = 3
def __init__(self):
# In production, use a database
self.tokens = {} # token_hash -> ResetToken
self.rate_limit = {} # email -> [timestamps]
def request_reset(self, email: str, user_id: Optional[int]) -> Optional[str]:
"""
Generate a password reset token.
Returns the token (to be emailed) or None if rate limited.
SECURITY: Always return the same response to the user,
regardless of whether the email exists.
"""
# Rate limiting
now = time.time()
if email in self.rate_limit:
recent = [t for t in self.rate_limit[email] if now - t < 3600]
if len(recent) >= self.MAX_REQUESTS_PER_HOUR:
return None # Rate limited
self.rate_limit[email] = recent
else:
self.rate_limit[email] = []
self.rate_limit[email].append(now)
# If user doesn't exist, return None silently
# (caller should still show "check your email" message)
if user_id is None:
return None
# Generate cryptographically secure token
token = secrets.token_urlsafe(32) # 256 bits of entropy
# Store HASH of token (not the token itself!)
# If database is breached, attacker can't use the hashes
token_hash = hashlib.sha256(token.encode()).hexdigest()
# Invalidate any existing tokens for this user
self.tokens = {
h: t for h, t in self.tokens.items()
if t.user_id != user_id
}
# Store new token
self.tokens[token_hash] = ResetToken(
token_hash=token_hash,
user_id=user_id,
created_at=now,
expires_at=now + (self.TOKEN_EXPIRY_MINUTES * 60),
)
return token # Send this in the email link
def verify_and_consume_token(self, token: str) -> Optional[int]:
"""
Verify a reset token and return the user_id.
Token is consumed (single-use).
Returns None if token is invalid/expired/used.
"""
token_hash = hashlib.sha256(token.encode()).hexdigest()
reset_token = self.tokens.get(token_hash)
if not reset_token:
return None
# Check expiration
if time.time() > reset_token.expires_at:
del self.tokens[token_hash]
return None
# Check if already used
if reset_token.used:
return None
# Mark as used
reset_token.used = True
# Clean up
del self.tokens[token_hash]
return reset_token.user_id
def cleanup_expired(self):
"""Remove expired tokens (run periodically)."""
now = time.time()
self.tokens = {
h: t for h, t in self.tokens.items()
if t.expires_at > now and not t.used
}
# Flask route example
from flask import Flask, request, jsonify
app = Flask(__name__)
reset_service = PasswordResetService()
@app.route('/api/forgot-password', methods=['POST'])
def forgot_password():
email = request.json.get('email', '').strip().lower()
if not email:
return jsonify({"error": "Email required"}), 400
# Look up user (may return None if not found)
user = find_user_by_email(email) # Your DB lookup
user_id = user.id if user else None
token = reset_service.request_reset(email, user_id)
if token and user:
# Send email with reset link
reset_link = f"https://myapp.com/reset-password?token={token}"
send_reset_email(email, reset_link) # Your email function
# ALWAYS return the same response (prevent email enumeration)
return jsonify({
"message": "If that email is registered, you will receive a reset link."
})
@app.route('/api/reset-password', methods=['POST'])
def reset_password():
token = request.json.get('token')
new_password = request.json.get('new_password')
if not token or not new_password:
return jsonify({"error": "Token and new password required"}), 400
# Validate new password
# (use password_policy.check_password_strength here)
user_id = reset_service.verify_and_consume_token(token)
if not user_id:
return jsonify({"error": "Invalid or expired token"}), 400
# Update password
update_user_password(user_id, new_password) # Hash and store
# Invalidate all existing sessions for this user
invalidate_all_sessions(user_id)
return jsonify({"message": "Password updated successfully"})
Key Security Properties:
| Property | Implementation |
|---|---|
| Token entropy | secrets.token_urlsafe(32) - 256 bits |
| Token storage | Store hash only, never plaintext |
| Single use | Token consumed on first use |
| Time-limited | 30-minute expiration |
| Rate limiting | Max 3 requests per hour per email |
| No enumeration | Same response whether email exists or not |
| Session invalidation | All sessions cleared after reset |
7. Biometric Authentication¶
7.1 Overview¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Biometric Authentication Types β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Physiological: β
β βββ Fingerprint - Most common, mature technology β
β βββ Face - Widespread on mobile (Face ID) β
β βββ Iris - High accuracy, expensive β
β βββ Retina - Very high accuracy, intrusive β
β βββ Palm/Vein - Contactless, increasingly popular β
β β
β Behavioral: β
β βββ Voice - Convenient for phone systems β
β βββ Typing rhythm - Continuous authentication β
β βββ Gait - Walking pattern recognition β
β βββ Signature - Dynamic analysis (pressure, speed) β
β β
β Key Metrics: β
β βββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββ β
β β FAR β False Acceptance Rate β β
β β β (impostor accepted as genuine) β β
β βββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€ β
β β FRR β False Rejection Rate β β
β β β (genuine user rejected) β β
β βββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€ β
β β EER β Equal Error Rate β β
β β β (where FAR = FRR; lower is better) β β
β βββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
7.2 Biometric Template Security¶
Biometric data requires special handling because, unlike passwords, biometric traits cannot be changed if compromised.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Biometric Template Protection β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β WRONG: Store raw biometric data β
β ββββββββββββ ββββββββββββββββ β
β β Raw scan βββββΆβ Store image β β If breached, game over β
β ββββββββββββ β in database β (can't change fingerprint) β
β ββββββββββββββββ β
β β
β CORRECT: Cancelable biometrics / template protection β
β ββββββββββββ ββββββββββββββββ βββββββββββββββ β
β β Raw scan βββββΆβ Extract βββββΆβ Transform β β
β ββββββββββββ β features β β (one-way, β β
β β (minutiae) β β cancelable)β β
β ββββββββββββββββ ββββββββ¬βββββββ β
β β β
β ββββββββΌβββββββ β
β β Store β β
β β template β β
β β (revocable) β β
β βββββββββββββββ β
β β
β On-Device Processing (preferred): β
β - Biometric matching happens on the device (Secure Enclave) β
β - Server never sees biometric data β
β - Device releases a cryptographic key upon match β
β - This is how Apple Face ID and Touch ID work β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
7.3 Biometric Tradeoffs¶
| Factor | Passwords | TOTP | Biometrics | FIDO2 |
|---|---|---|---|---|
| Can be changed | Yes | Yes (re-enroll) | No | Yes (re-register) |
| Can be shared | Yes (bad) | Yes (bad) | Difficult | No |
| Can be forgotten | Yes | N/A | No | N/A |
| Spoofing risk | Phishing | Phishing | Presentation attack | Very low |
| Privacy concern | Low | Low | High | Low |
| Best used as | Primary factor | 2nd factor | 2nd factor (local) | 2nd factor or passwordless |
8. Authentication Architecture Patterns¶
8.1 Choosing the Right Pattern¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authentication Pattern Decision Tree β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β What type of application? β
β β β
β βββ Traditional web app (server-rendered) β
β β βββ Use: Server-side sessions + HttpOnly cookies β
β β β
β βββ SPA (React, Vue, etc.) β
β β βββ Use: OAuth 2.0 + PKCE β
β β Store access token in memory β
β β Store refresh token in HttpOnly cookie β
β β β
β βββ Mobile app β
β β βββ Use: OAuth 2.0 + PKCE + Secure storage β
β β (iOS Keychain, Android Keystore) β
β β β
β βββ API-to-API (service mesh) β
β β βββ Use: Client Credentials flow + mTLS β
β β Or: Service mesh (Istio) with automatic mTLS β
β β β
β βββ Microservices β
β βββ Use: JWT (RS256) with centralized auth service β
β Auth service issues tokens β
β Each service verifies with public key β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
8.2 Centralized Authentication (Auth Service Pattern)¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Microservices Authentication Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β ββββββββββββββββ β
β β API β β
β β Gateway β β
β ββββββββ¬ββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Service β β Service β β Service β β
β β A β β B β β C β β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β β β β
β ββββββββββββββββΌβββββββββββββββ β
β β β
β βΌ β
β ββββββββββββββββ β
β β Auth β β
β β Service β β
β β βββββββββββ β β
β β - Login β β
β β - Token β β
β β issuance β β
β β - User β β
β β managementβ β
β β - JWKS β β
β β endpoint β β
β ββββββββββββββββ β
β β
β Flow: β
β 1. Client authenticates with Auth Service β gets JWT β
β 2. Client sends JWT to API Gateway β
β 3. Gateway validates JWT signature (using public key from JWKS) β
β 4. Gateway forwards request + JWT claims to services β
β 5. Services trust validated claims without re-validating β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
9. Exercises¶
Exercise 1: Implement Secure Password Storage¶
Build a complete user registration and login system:
"""
Exercise: Implement the following UserService class.
Use argon2 for password hashing.
Include input validation and proper error handling.
"""
class UserService:
def register(self, username: str, email: str, password: str) -> dict:
"""
Register a new user.
- Validate password strength (min 12 chars, not common)
- Check username/email uniqueness
- Hash password with argon2id
- Return user info (without password hash)
"""
pass
def login(self, username: str, password: str) -> dict:
"""
Authenticate a user.
- Verify credentials
- Implement account lockout after 5 failed attempts
- Regenerate session on successful login
- Return access + refresh tokens
"""
pass
def change_password(self, user_id: int, old_password: str,
new_password: str) -> bool:
"""
Change user's password.
- Verify old password
- Validate new password
- Invalidate all existing sessions
"""
pass
Exercise 2: TOTP Integration¶
Add TOTP-based 2FA to the UserService:
"""
Exercise: Add these methods to UserService.
Use the pyotp library.
"""
class UserService:
# ... (from Exercise 1)
def enable_2fa(self, user_id: int) -> dict:
"""
Enable TOTP 2FA for a user.
Returns: {"secret": ..., "qr_uri": ..., "backup_codes": [...]}
"""
pass
def verify_2fa_setup(self, user_id: int, code: str) -> bool:
"""Verify initial TOTP code to confirm setup."""
pass
def login_2fa(self, username: str, password: str,
totp_code: str) -> dict:
"""
Login with 2FA.
- First verify password
- Then verify TOTP code
- Support backup codes as fallback
"""
pass
Exercise 3: JWT Security Audit¶
Identify and fix the security issues in this code:
"""
Exercise: Find and fix ALL security issues in this JWT implementation.
"""
import jwt
import time
SECRET = "mysecret" # Issue 1: ???
def create_token(user_id):
payload = {
"user_id": user_id,
"password": get_user_password(user_id), # Issue 2: ???
"admin": False,
}
return jwt.encode(payload, SECRET) # Issue 3: ???
def verify_token(token):
try:
payload = jwt.decode(token, SECRET, algorithms=["HS256", "none"]) # Issue 4: ???
return payload
except: # Issue 5: ???
return None
def protected_route(token):
payload = verify_token(token)
if payload:
if payload.get("admin"): # Issue 6: ???
return admin_dashboard()
return user_dashboard(payload["user_id"])
return "Unauthorized"
Exercise 4: OAuth 2.0 Flow Implementation¶
Implement a complete OAuth 2.0 client with PKCE:
"""
Exercise: Complete this OAuth 2.0 client implementation.
Include PKCE, state validation, and secure token storage.
"""
class OAuthClient:
def __init__(self, client_id: str, auth_url: str,
token_url: str, redirect_uri: str):
pass
def start_auth_flow(self) -> str:
"""
Generate the authorization URL.
Include PKCE code_challenge and state parameter.
Returns the URL to redirect the user to.
"""
pass
def handle_callback(self, callback_url: str) -> dict:
"""
Handle the OAuth callback.
- Verify state parameter
- Exchange code for tokens using code_verifier
- Return tokens
"""
pass
def refresh_access_token(self, refresh_token: str) -> dict:
"""Refresh an expired access token."""
pass
Exercise 5: Password Reset Security Review¶
Review this password reset flow and list all security issues:
"""
Exercise: Identify ALL security vulnerabilities in this code.
Write a corrected version.
"""
from flask import Flask, request
import random
import string
app = Flask(__name__)
reset_codes = {} # email -> code
@app.route('/forgot', methods=['POST'])
def forgot_password():
email = request.form['email']
user = db.find_user(email=email)
if not user:
return "Email not found", 404 # Issue: ???
# Generate 4-digit reset code
code = ''.join(random.choices(string.digits, k=4)) # Issue: ???
reset_codes[email] = code # Issue: ???
send_email(email, f"Your reset code is: {code}")
return "Code sent"
@app.route('/reset', methods=['POST'])
def reset_password():
email = request.form['email']
code = request.form['code']
new_password = request.form['password']
if reset_codes.get(email) == code: # Issue: ???
user = db.find_user(email=email)
user.password = new_password # Issue: ???
db.save(user)
return "Password updated"
return "Invalid code", 400
10. Summary¶
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Authentication Systems Summary β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β Password Storage: β
β - Use Argon2id or bcrypt (NEVER MD5/SHA1/SHA256 alone) β
β - Automatic salting, key stretching β
β - Follow NIST SP 800-63B guidelines β
β β
β Multi-Factor Auth: β
β - TOTP is the minimum recommended 2nd factor β
β - FIDO2/WebAuthn is the gold standard (phishing-resistant) β
β - SMS is the weakest 2nd factor (SIM swapping) β
β - Always provide backup codes for account recovery β
β β
β OAuth 2.0 / OIDC: β
β - Use Authorization Code flow with PKCE β
β - Always validate state parameter (CSRF) β
β - OIDC adds identity layer (ID tokens) on top of OAuth β
β β
β Sessions & JWT: β
β - HttpOnly + Secure + SameSite cookies β
β - Regenerate session ID after login β
β - JWT: short-lived access + long-lived refresh β
β - Always specify allowed algorithms in JWT verification β
β - Never store sensitive data in JWT payload β
β β
β Password Reset: β
β - Cryptographically random tokens (256+ bits) β
β - Store hash of token, not token itself β
β - Single-use, time-limited (30 min) β
β - Same response regardless of email existence β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Previous: 04. TLS/SSL and Public Key Infrastructure | Next: 06. Authorization and Access Control