auth_demo.py

Download
python 556 lines 16.9 KB
  1"""
  2Authentication Mechanisms Demo
  3==============================
  4
  5Educational demonstration of authentication concepts:
  6- JWT (JSON Web Token) creation and verification (manual, no PyJWT)
  7- TOTP (Time-based One-Time Password) from scratch
  8- Password strength checker
  9- Secure token generation for password resets
 10- Session ID generation
 11
 12Uses only Python standard library (base64, hmac, hashlib, secrets, time).
 13No external dependencies required.
 14"""
 15
 16import base64
 17import hashlib
 18import hmac
 19import json
 20import math
 21import os
 22import re
 23import secrets
 24import struct
 25import time
 26import string
 27from datetime import datetime, timezone, timedelta
 28
 29print("=" * 65)
 30print("  Authentication Mechanisms Demo")
 31print("=" * 65)
 32print()
 33
 34
 35# ============================================================
 36# Section 1: JWT (JSON Web Token) - Manual Implementation
 37# ============================================================
 38
 39print("-" * 65)
 40print("  Section 1: JWT (JSON Web Token) Implementation")
 41print("-" * 65)
 42
 43print("""
 44  JWT Structure: header.payload.signature
 45  - Header:    {"alg": "HS256", "typ": "JWT"}  (base64url)
 46  - Payload:   Claims (iss, sub, exp, iat, ...)  (base64url)
 47  - Signature: HMAC-SHA256(header.payload, secret)
 48""")
 49
 50
 51def base64url_encode(data: bytes) -> str:
 52    """Base64url encoding without padding (JWT standard)."""
 53    return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
 54
 55
 56def base64url_decode(s: str) -> bytes:
 57    """Base64url decoding with padding restoration."""
 58    padding = 4 - len(s) % 4
 59    if padding != 4:
 60        s += "=" * padding
 61    return base64.urlsafe_b64decode(s)
 62
 63
 64def jwt_create(payload: dict, secret: str, exp_minutes: int = 60) -> str:
 65    """Create a JWT token with HS256 signature."""
 66    # Header
 67    header = {"alg": "HS256", "typ": "JWT"}
 68    header_b64 = base64url_encode(json.dumps(header, separators=(",", ":")).encode())
 69
 70    # Add standard claims
 71    now = int(time.time())
 72    payload = {
 73        **payload,
 74        "iat": now,
 75        "exp": now + exp_minutes * 60,
 76    }
 77    payload_b64 = base64url_encode(json.dumps(payload, separators=(",", ":")).encode())
 78
 79    # Signature
 80    signing_input = f"{header_b64}.{payload_b64}"
 81    signature = hmac.new(
 82        secret.encode(), signing_input.encode(), hashlib.sha256
 83    ).digest()
 84    signature_b64 = base64url_encode(signature)
 85
 86    return f"{header_b64}.{payload_b64}.{signature_b64}"
 87
 88
 89def jwt_verify(token: str, secret: str) -> dict:
 90    """Verify and decode a JWT token."""
 91    parts = token.split(".")
 92    if len(parts) != 3:
 93        raise ValueError("Invalid JWT format: expected 3 parts")
 94
 95    header_b64, payload_b64, signature_b64 = parts
 96
 97    # Verify signature
 98    signing_input = f"{header_b64}.{payload_b64}"
 99    expected_sig = hmac.new(
100        secret.encode(), signing_input.encode(), hashlib.sha256
101    ).digest()
102    actual_sig = base64url_decode(signature_b64)
103
104    if not hmac.compare_digest(expected_sig, actual_sig):
105        raise ValueError("Invalid signature")
106
107    # Decode header and verify algorithm
108    header = json.loads(base64url_decode(header_b64))
109    if header.get("alg") != "HS256":
110        raise ValueError(f"Unsupported algorithm: {header.get('alg')}")
111
112    # Decode payload
113    payload = json.loads(base64url_decode(payload_b64))
114
115    # Check expiration
116    if "exp" in payload and payload["exp"] < int(time.time()):
117        raise ValueError("Token has expired")
118
119    return payload
120
121
122# Demo
123jwt_secret = "super-secret-key-change-in-production"
124claims = {
125    "sub": "user_12345",
126    "name": "Alice",
127    "role": "admin",
128    "iss": "auth.example.com",
129}
130
131token = jwt_create(claims, jwt_secret, exp_minutes=30)
132print(f"\n  JWT Token:")
133parts = token.split(".")
134print(f"    Header:    {parts[0][:40]}...")
135print(f"    Payload:   {parts[1][:40]}...")
136print(f"    Signature: {parts[2]}")
137print(f"    Full ({len(token)} chars): {token[:50]}...")
138print()
139
140# Decode and display
141decoded_header = json.loads(base64url_decode(parts[0]))
142decoded_payload = json.loads(base64url_decode(parts[1]))
143print(f"  Decoded Header:  {json.dumps(decoded_header)}")
144print(f"  Decoded Payload: {json.dumps(decoded_payload, indent=2)}")
145print()
146
147# Verify valid token
148try:
149    verified = jwt_verify(token, jwt_secret)
150    print(f"  Verification:    VALID")
151    print(f"  User:            {verified['name']} ({verified['sub']})")
152    print(f"  Role:            {verified['role']}")
153except ValueError as e:
154    print(f"  Verification:    FAILED - {e}")
155
156# Verify with wrong secret
157try:
158    jwt_verify(token, "wrong-secret")
159    print(f"  Wrong secret:    VALID (should not happen!)")
160except ValueError as e:
161    print(f"  Wrong secret:    FAILED - {e}")
162
163# Verify expired token
164expired_token = jwt_create(claims, jwt_secret, exp_minutes=-1)
165try:
166    jwt_verify(expired_token, jwt_secret)
167    print(f"  Expired token:   VALID (should not happen!)")
168except ValueError as e:
169    print(f"  Expired token:   FAILED - {e}")
170
171print()
172
173
174# ============================================================
175# Section 2: TOTP (Time-based One-Time Password)
176# ============================================================
177
178print("-" * 65)
179print("  Section 2: TOTP (Time-based One-Time Password)")
180print("-" * 65)
181
182print("""
183  TOTP (RFC 6238) generates 6-digit codes that change every 30s.
184  Used by: Google Authenticator, Authy, 1Password, etc.
185
186  Algorithm:
187  1. shared_secret = random 20+ bytes (base32 encoded)
188  2. counter = floor(current_time / 30)
189  3. hmac = HMAC-SHA1(secret, counter_as_8_bytes)
190  4. offset = hmac[-1] & 0x0F
191  5. code = (hmac[offset:offset+4] & 0x7FFFFFFF) % 10^digits
192""")
193
194
195def generate_totp_secret(length: int = 20) -> bytes:
196    """Generate a random TOTP secret."""
197    return os.urandom(length)
198
199
200def totp_generate(secret: bytes, time_step: int = 30, digits: int = 6,
201                  timestamp: float = None) -> str:
202    """Generate a TOTP code (RFC 6238)."""
203    if timestamp is None:
204        timestamp = time.time()
205
206    # Step 1: Calculate time counter
207    counter = int(timestamp) // time_step
208
209    # Step 2: HMAC-SHA1 of counter
210    counter_bytes = struct.pack(">Q", counter)  # 8-byte big-endian
211    hmac_digest = hmac.new(secret, counter_bytes, hashlib.sha1).digest()
212
213    # Step 3: Dynamic truncation
214    offset = hmac_digest[-1] & 0x0F
215    binary_code = struct.unpack(">I", hmac_digest[offset:offset + 4])[0]
216    binary_code &= 0x7FFFFFFF  # Remove sign bit
217
218    # Step 4: Modulo to get desired digits
219    otp = binary_code % (10 ** digits)
220    return str(otp).zfill(digits)
221
222
223def totp_verify(secret: bytes, code: str, time_step: int = 30,
224                window: int = 1) -> bool:
225    """Verify a TOTP code with a time window for clock skew."""
226    now = time.time()
227    for offset in range(-window, window + 1):
228        check_time = now + offset * time_step
229        expected = totp_generate(secret, time_step, len(code), check_time)
230        if hmac.compare_digest(code, expected):
231            return True
232    return False
233
234
235# Demo
236totp_secret = generate_totp_secret()
237secret_b32 = base64.b32encode(totp_secret).decode()
238
239print(f"\n  Secret (base32): {secret_b32}")
240print(f"  Secret (hex):    {totp_secret.hex()}")
241print()
242
243# Generate current code
244current_time = time.time()
245current_code = totp_generate(totp_secret, timestamp=current_time)
246time_step = 30
247remaining = time_step - (int(current_time) % time_step)
248
249print(f"  Current TOTP:    {current_code}")
250print(f"  Valid for:       {remaining}s (of {time_step}s window)")
251print()
252
253# Show codes across time windows
254print("  TOTP codes across time windows:")
255for offset in range(-2, 3):
256    t = current_time + offset * 30
257    code = totp_generate(totp_secret, timestamp=t)
258    marker = " <-- current" if offset == 0 else ""
259    label = f"T{offset:+d}" if offset != 0 else "T=0"
260    print(f"    {label}: {code}{marker}")
261print()
262
263# Verify
264is_valid = totp_verify(totp_secret, current_code)
265is_invalid = totp_verify(totp_secret, "000000")
266print(f"  Verify '{current_code}': {is_valid}")
267print(f"  Verify '000000': {is_invalid}")
268print()
269
270# QR code URL format (for authenticator apps)
271account = "user@example.com"
272issuer = "MyApp"
273otpauth_url = (
274    f"otpauth://totp/{issuer}:{account}"
275    f"?secret={secret_b32}&issuer={issuer}&algorithm=SHA1&digits=6&period=30"
276)
277print(f"  QR Code URL (for authenticator apps):")
278print(f"    {otpauth_url}")
279print()
280
281
282# ============================================================
283# Section 3: Password Strength Checker
284# ============================================================
285
286print("-" * 65)
287print("  Section 3: Password Strength Checker")
288print("-" * 65)
289
290
291def check_password_strength(password: str) -> dict:
292    """Evaluate password strength with detailed feedback."""
293    score = 0
294    feedback = []
295    checks = {}
296
297    # Length check
298    length = len(password)
299    checks["length"] = length
300    if length >= 16:
301        score += 3
302    elif length >= 12:
303        score += 2
304    elif length >= 8:
305        score += 1
306    else:
307        feedback.append("Use at least 8 characters (12+ recommended)")
308
309    # Character class checks
310    has_lower = bool(re.search(r"[a-z]", password))
311    has_upper = bool(re.search(r"[A-Z]", password))
312    has_digit = bool(re.search(r"\d", password))
313    has_special = bool(re.search(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]", password))
314
315    checks["lowercase"] = has_lower
316    checks["uppercase"] = has_upper
317    checks["digits"] = has_digit
318    checks["special"] = has_special
319
320    char_classes = sum([has_lower, has_upper, has_digit, has_special])
321    score += char_classes
322    if not has_lower:
323        feedback.append("Add lowercase letters")
324    if not has_upper:
325        feedback.append("Add uppercase letters")
326    if not has_digit:
327        feedback.append("Add numbers")
328    if not has_special:
329        feedback.append("Add special characters (!@#$%^&*...)")
330
331    # Common pattern checks
332    common_patterns = [
333        (r"(.)\1{2,}", "Avoid repeated characters (aaa, 111)"),
334        (r"(012|123|234|345|456|567|678|789)", "Avoid sequential numbers"),
335        (r"(abc|bcd|cde|def|efg|fgh|ghi)", "Avoid sequential letters"),
336        (r"(?i)(password|qwerty|admin|login|welcome)", "Avoid common words"),
337    ]
338
339    for pattern, message in common_patterns:
340        if re.search(pattern, password):
341            score -= 1
342            feedback.append(message)
343
344    # Entropy estimation
345    pool_size = 0
346    if has_lower:
347        pool_size += 26
348    if has_upper:
349        pool_size += 26
350    if has_digit:
351        pool_size += 10
352    if has_special:
353        pool_size += 32
354    entropy = length * math.log2(pool_size) if pool_size > 0 else 0
355    checks["entropy_bits"] = round(entropy, 1)
356
357    # Determine strength level
358    score = max(0, min(score, 7))
359    if score >= 6:
360        strength = "STRONG"
361    elif score >= 4:
362        strength = "MODERATE"
363    elif score >= 2:
364        strength = "WEAK"
365    else:
366        strength = "VERY WEAK"
367
368    return {
369        "password": password[:2] + "*" * (len(password) - 2),
370        "score": score,
371        "max_score": 7,
372        "strength": strength,
373        "entropy_bits": checks["entropy_bits"],
374        "checks": checks,
375        "feedback": feedback if feedback else ["Good password!"],
376    }
377
378
379# Test various passwords
380test_passwords = [
381    "password",
382    "Admin123",
383    "MyD0g$N@me!",
384    "correct-horse-battery-staple",
385    "Tr0ub4dor&3",
386    "j&Hx9#mK2$pL@nQ!",
387]
388
389print()
390for pwd in test_passwords:
391    result = check_password_strength(pwd)
392    bar = "#" * result["score"] + "." * (result["max_score"] - result["score"])
393    print(f"  Password: {result['password']:<22} [{bar}] {result['strength']}")
394    print(f"    Entropy: {result['entropy_bits']} bits, Score: {result['score']}/{result['max_score']}")
395    if result["feedback"] and result["feedback"][0] != "Good password!":
396        for fb in result["feedback"][:2]:
397            print(f"    - {fb}")
398    print()
399
400print("  Entropy benchmarks:")
401print("    < 28 bits:  Trivially crackable")
402print("    28-35 bits: Crackable with effort")
403print("    36-59 bits: Reasonable for online attacks")
404print("    60-127 bits: Strong against offline attacks")
405print("    128+ bits:  Computationally infeasible")
406print()
407
408
409# ============================================================
410# Section 4: Secure Token Generation
411# ============================================================
412
413print("-" * 65)
414print("  Section 4: Secure Token Generation")
415print("-" * 65)
416
417print("""
418  Tokens must be cryptographically random (not predictable).
419  Python's `secrets` module uses OS-level CSPRNG.
420""")
421
422# Password reset token
423reset_token = secrets.token_urlsafe(32)
424print(f"  Password Reset Token:  {reset_token}")
425print(f"    Length:              {len(reset_token)} chars")
426print(f"    Entropy:             256 bits")
427print()
428
429# Email verification token
430email_token = secrets.token_hex(16)
431print(f"  Email Verify Token:    {email_token}")
432print(f"    Length:              {len(email_token)} chars")
433print()
434
435# API key generation
436def generate_api_key(prefix: str = "sk") -> str:
437    """Generate an API key with prefix (like Stripe sk_live_...)."""
438    random_part = secrets.token_urlsafe(32)
439    return f"{prefix}_{random_part}"
440
441api_key = generate_api_key("sk_live")
442print(f"  API Key:               {api_key}")
443print()
444
445# Token with expiration (stored in DB)
446def create_token_record(purpose: str, ttl_hours: int = 24) -> dict:
447    """Create a token record for database storage."""
448    token = secrets.token_urlsafe(32)
449    token_hash = hashlib.sha256(token.encode()).hexdigest()
450    now = datetime.now(timezone.utc)
451    return {
452        "token_plaintext": token,  # Send to user (email, etc.)
453        "token_hash": token_hash,  # Store in DB (never store plaintext!)
454        "purpose": purpose,
455        "created_at": now.isoformat(),
456        "expires_at": (now + timedelta(hours=ttl_hours)).isoformat(),
457    }
458
459record = create_token_record("password_reset", ttl_hours=1)
460print(f"  Token Record (for DB storage):")
461print(f"    Plaintext (sent):  {record['token_plaintext'][:30]}...")
462print(f"    Hash (stored):     {record['token_hash'][:32]}...")
463print(f"    Purpose:           {record['purpose']}")
464print(f"    Expires:           {record['expires_at']}")
465print()
466print("  IMPORTANT: Store only the HASH in the database.")
467print("  When user presents token, hash it and compare to stored hash.")
468print()
469
470
471# ============================================================
472# Section 5: Session ID Generation
473# ============================================================
474
475print("-" * 65)
476print("  Section 5: Session ID Generation")
477print("-" * 65)
478
479print("""
480  Session IDs must be:
481  - Cryptographically random (unpredictable)
482  - Sufficiently long (128+ bits of entropy)
483  - Unique across all active sessions
484  - Regenerated after authentication changes
485""")
486
487
488def generate_session_id() -> str:
489    """Generate a secure session ID (128 bits of entropy)."""
490    return secrets.token_hex(16)
491
492
493def generate_session_with_metadata() -> dict:
494    """Create a session with metadata for server-side storage."""
495    session_id = secrets.token_hex(32)
496    return {
497        "session_id": session_id,
498        "created_at": datetime.now(timezone.utc).isoformat(),
499        "last_accessed": datetime.now(timezone.utc).isoformat(),
500        "ip_address": "192.168.1.100",  # From request
501        "user_agent": "Mozilla/5.0...",  # From request headers
502        "user_id": None,  # Set after login
503        "is_authenticated": False,
504    }
505
506
507session = generate_session_with_metadata()
508print(f"\n  Session ID:      {session['session_id'][:32]}...")
509print(f"  Created:         {session['created_at']}")
510print(f"  Authenticated:   {session['is_authenticated']}")
511print()
512
513# Show multiple unique session IDs
514print("  Sample session IDs (all unique):")
515for i in range(5):
516    sid = generate_session_id()
517    print(f"    {i+1}. {sid}")
518print()
519
520# Session cookie attributes
521print("  Secure session cookie attributes:")
522print("    Set-Cookie: session_id=<token>;")
523print("                HttpOnly;    -- no JavaScript access")
524print("                Secure;      -- HTTPS only")
525print("                SameSite=Lax; -- CSRF protection")
526print("                Path=/;")
527print("                Max-Age=3600; -- 1 hour expiry")
528print()
529
530
531# ============================================================
532# Section 6: Summary
533# ============================================================
534
535print("=" * 65)
536print("  Summary")
537print("=" * 65)
538print("""
539  Mechanism        | Use Case              | Key Points
540  -----------------+-----------------------+---------------------------
541  JWT (HS256)      | Stateless auth        | Short-lived, secret key
542  TOTP             | 2FA / MFA             | Time-based, 30s window
543  Password check   | Registration/update   | Entropy, patterns, length
544  Reset tokens     | Password recovery     | Hash before storing
545  Session IDs      | Stateful auth         | Random, HttpOnly, Secure
546
547  Authentication Best Practices:
548  - Always use MFA/2FA (TOTP or WebAuthn)
549  - Hash password reset tokens before DB storage
550  - Set short expiration for sensitive tokens
551  - Regenerate session IDs after login/logout
552  - Use HttpOnly + Secure + SameSite cookies
553  - Rate-limit authentication endpoints
554  - Log all authentication events
555""")