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

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                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    β”‚                           β”‚
β”‚     │◀───────────────────────────────│                           β”‚
β”‚                                                                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
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

to navigate between lessons