Latest Intelligence

๐Ÿ” OWASP Top 10 โ€” A07:2021 Identification and Authentication Failures

10/10/2026
Research Report

๐Ÿ” OWASP Top 10 โ€” A07:2021 Identification and Authentication Failures

The Login Page Is Your Front Door: Why 80% of Breaches Start With Broken Authentication

"Passwords are like underwear: change them often, don't share them, and never leave them lying around. But most importantly โ€” stop relying on them alone."

>

This blog dissects A07:2021 โ€” Identification and Authentication Failures down to the atomic level. By the end, you will understand why credential stuffing attacks succeed 2% of the time (and that's a disaster at scale), how session hijacking actually works, and how to build authentication systems that attackers simply cannot break.

๐Ÿ“– Table of Contents

  1. Introduction โ€” Why Authentication Is the #1 Target
  2. What Exactly Is A07?
  3. Identification vs Authentication vs Authorization
  4. The Authentication Attack Landscape
  5. Real-World Breaches
  6. Technical Deep-Dive: CWE Mapping
  7. Password-Based Authentication: Done Right
  8. Multi-Factor Authentication (MFA)
  9. Session Management: The Forgotten Battlefield
  10. JWT Security: The Modern Pitfalls
  11. OAuth 2.0 & OIDC: Doing It Right
  12. Passwordless Authentication: The Future
  13. Account Recovery: The Weakest Link
  14. Code Examples: Vulnerable vs Secure
  15. The Ultimate Checklist
  16. Conclusion
  17. ๐Ÿšช Introduction โ€” Why Authentication Is the #1 Target

Authentication is the front door of every application. It's the one component that every user interacts with, that every attacker probes first, and that is most often implemented incorrectly.

๐Ÿ”ด According to Verizon's 2023 Data Breach Investigations Report:
  • 80% of hacking-related breaches involve stolen or brute-forced credentials
  • 74% of all breaches include a human element (often tricked into revealing credentials)
  • Credential stuffing attacks increased 65% year-over-year
  • The average cost of a credential-related breach is $4.5 million

The Harsh Reality

text
Every day:
  ๐Ÿค– Bots make BILLIONS of login attempts across the internet
  ๐Ÿ’€ Attackers test LEAKED credentials from previous breaches
  ๐Ÿ“ฑ Phishing kits capture MILLIONS of passwords
  ๐Ÿ”“ Session tokens are stolen through XSS, MITM, or malware
  ๐ŸŽญ Account takeovers happen EVERY 2 SECONDS globally

The Statistics

MetricValue
Incidence Rate (tested applications)2.55%
Mapped CWEs22
Max Incidence Rate14.84%
Average Weighted CVSS Score7.40
% of breaches involving credentials80%+
Average credential stuffing success rate0.1% - 2%
Known compromised passwords (HIBP)850+ million

A 0.1% success rate sounds small โ€” until you realize attackers try millions of credentials per hour. 0.1% of 10 million = 10,000 compromised accounts per day.

Why This Category Was Renamed

In OWASP 2017, this was called "Broken Authentication." In 2021, it was renamed to "Identification and Authentication Failures" to acknowledge that:

  1. Identification (knowing WHO is making a request) is distinct from
  2. Authentication (proving they ARE who they claim to be)

Both can fail independently, and both are equally dangerous.

  1. ๐Ÿง  What Exactly Is A07?

Identification and Authentication Failures covers vulnerabilities related to:

  • ๐Ÿ”ด Confirming user identity incorrectly (or not at all)
  • ๐ŸŸ  Weak credential management (passwords, tokens, keys)
  • ๐ŸŸก Broken session handling (session fixation, hijacking)
  • ๐ŸŸข Missing or weak MFA
  • ๐Ÿ”ต Vulnerable password recovery mechanisms
  • ๐ŸŸฃ Poor credential storage (plaintext passwords, weak hashing)

The Official OWASP Definition

"Confirmation of the user's identity, authentication, and session management is critical to protect against authentication-related attacks. There may be authentication weaknesses if the application permits automated attacks, uses default credentials, uses weak password recovery processes, stores passwords insecurely, or has broken session management."

The Three Pillars of Secure Authentication

  1. โš–๏ธ Identification vs Authentication vs Authorization

These three concepts are constantly confused, even by experienced developers. Understanding them is foundational.

The Analogy: Airport Security

StepReal WorldDigital Equivalent
1. Identification"I am John Smith" (shows name on boarding pass)Username/email entered
2. AuthenticationShows passport matching the namePassword, MFA, biometric verification
3. AuthorizationBoarding pass says "Gate 5, Seat 12A"Role/permission check determines access

What A07 Covers

text
A07 โ€” Identification and Authentication Failures covers:
โ”œโ”€โ”€ Identification: Weak user discovery/enumeration prevention
โ”œโ”€โ”€ Authentication: Broken login mechanisms
โ””โ”€โ”€ Session Management: Compromised session handling

A01 โ€” Broken Access Control covers:
โ””โ”€โ”€ Authorization: Who can do what (separate concern)

The Common Mistake

python
# ๐Ÿ”ด WRONG โ€” Confusing authentication with authorization
if user.username == "admin":  # โ† This is IDENTIFICATION only
    allow_admin_access()        # โ† Should check AUTHORIZATION

# ๐ŸŸข RIGHT โ€” Separate concerns
if authenticate(username, password, mfa_token):  # Authentication
    if user.has_permission("admin:access"):      # Authorization
        allow_admin_access()
  1. ๐Ÿ’€ The Authentication Attack Landscape

Attackers have an entire arsenal targeting authentication. Here are the big ones:

Attack #1: Credential Stuffing

Why it works: 65% of people reuse passwords across sites.

Attack #2: Brute Force

text
Target: Admin account with 8-character alphanumeric password

Password space: 36^8 = ~2.8 trillion combinations
Attempts per second (modern GPU): ~100 billion
Time to crack: 2.8 trillion / 100 billion = 28 seconds

Reality check: Online brute force is slower (rate limiting),
but OFFLINE brute force (after stealing hash) is terrifying.

Attack #3: Password Spraying

text
๐Ÿ”ด Traditional brute force (fails to lockout):
  Try: [email protected], password1, password2, password3, ... (BLOCKED)

๐ŸŸข Password spraying (bypasses lockout):
  Try: [email protected], Winter2024!
  Try: [email protected], Winter2024!
  Try: [email protected], Winter2024!
  Try: ... 10,000 users, same password

Why it works: Account lockout triggers on failed attempts per USER,
not per PASSWORD. Trying one common password across many users
avoids triggering lockouts.

Attack #4: Session Hijacking

Attack #5: Session Fixation

text
๐Ÿ”ด VULNERABLE FLOW:

1. Attacker visits site, gets session ID: SESSIONID=xyz789
2. Attacker tricks victim into using same session:
   Email: "Click here! https:">//site.com/login?SESSIONID=xyz789"
3. Victim logs in โ€” session ID xyz789 is now authenticated
4. Attacker also uses SESSIONID=xyz789 โ†’ authenticated as victim

๐ŸŸข FIX: Regenerate session ID on login

Attack #6: Phishing

text
๐ŸŽฃ Modern phishing kits:
  - Clone target website perfectly (1-click deployment)
  - Capture credentials in real-time
  - Even bypass MFA (relay attacks)
  - Use lookalike domains: paypal-security.com, 
    rnicrosoft.com (rn = m), amaz0n.com

Statistics:
  - 1 in 4 people click phishing links
  - 1 in 10 submit credentials
  - Average phishing campaign ROI: 1500%

Attack #7: MFA Bypass

text
Common MFA bypasses:
  ๐Ÿ”ด SMS interception (SIM swapping)
  ๐Ÿ”ด Push notification fatigue (spam approvals)
  ๐Ÿ”ด OAuth consent phishing
  ๐Ÿ”ด Session cookie theft (bypasses MFA entirely)
  ๐Ÿ”ด Account recovery flow (circumvents MFA)
  ๐Ÿ”ด MFA removal via support social engineering

Attack #8: Account Enumeration

http
๐Ÿ”ด VULNERABLE โ€” Different responses reveal account existence:

POST /login
email: [email protected], password: wrong
Response: "Invalid password"           โ† Account EXISTS

POST /login
email: [email protected], password: wrong
Response: "User not found"             โ† Account doesn't exist

Attacker now has a list of valid email addresses to target.
  1. ๐Ÿ”ฅ Real-World Breaches

๐Ÿ“Œ Case 1: 23andMe (2023) โ€” 6.9 Million Users via Credential Stuffing

The Attack:

  • Attackers used credentials leaked from other breaches
  • Logged into 14,000 accounts directly via credential stuffing
  • Through DNA relative sharing feature, accessed data of 6.9 million additional users
  • Data included genetic ancestry, health predispositions, family trees

The Root Cause:

  • No MFA enforcement
  • No credential stuffing detection
  • No anomalous login monitoring
  • Users reused passwords from previous breaches

The Aftermath:

  • Class action lawsuits
  • $30 million settlement
  • Permanent reputation damage
  • Forced MFA rollout (too late)

๐Ÿ“Œ Case 2: Colonial Pipeline (2021) โ€” Single Password Shuts Down US Fuel Supply

The Attack:

  • Attackers obtained a single VPN password (likely from dark web)
  • VPN account had no MFA enabled
  • Account was inactive but never deactivated
  • Ransomware deployed, pipeline shut down for 6 days

The Impact:

  • Fuel shortages across US East Coast
  • Gas prices spiked
  • State of emergency declared in 17 states
  • $4.4 million ransom paid (partially recovered)

The Lesson: One missing MFA. One forgotten account. National infrastructure crisis.

๐Ÿ“Œ Case 3: Twitter (2020) โ€” Admin Panel via Social Engineering

The Attack:

  • Attackers social-engineered Twitter employees via phone
  • Gained access to internal admin panel
  • Panel allowed changing email addresses on any account
  • Took over accounts: Obama, Biden, Musk, Bezos, Apple, Uber, etc.
  • Posted cryptocurrency scams, netted $120,000+ in minutes

The Root Cause:

  • Admin panel didn't require MFA for sensitive actions
  • Internal tools lacked proper access controls
  • No anomaly detection on account takeovers
  • Insider access was over-privileged

๐Ÿ“Œ Case 4: Microsoft Exchange (2021) โ€” ProxyLogon

The Attack:

  • Chain of 4 vulnerabilities including authentication bypass
  • Attackers forged server-side request to Exchange's internal API
  • Bypassed authentication entirely
  • Gained SYSTEM-level access
  • 250,000+ servers compromised globally

The Lesson: Even well-designed authentication can be bypassed if any component fails.

๐Ÿ“Œ Case 5: Okta (2022) โ€” Session Token Theft

The Attack:

  • Attackers compromised a Okta support engineer's laptop
  • Stole session cookies from browser
  • Used cookies to access customer admin panels
  • 366 customers affected

The Lesson: Session tokens are bearer tokens โ€” whoever has them, IS the user. Protect them like passwords.

  1. ๐Ÿ—บ๏ธ Technical Deep-Dive: CWE Mapping

A07:2021 maps to 22 CWEs. The most critical:

The Critical CWEs Explained

๐Ÿ”ธ CWE-521: Weak Password Requirements

text
๐Ÿ”ด EXAMPLES:
- Minimum 6 characters
- No complexity requirements
- No breach check
- Common passwords allowed ("password123")

๐ŸŸข MODERN REQUIREMENTS (NIST SP 800-63B):
- Minimum 8 characters (12+ preferred)
- Check against breach databases (HaveIBeenPwned)
- No arbitrary complexity rules (per NIST)
- No periodic forced resets (unless breach suspected)
- Allow passphrases and all printable characters

๐Ÿ”ธ CWE-307: Improper Restriction of Excessive Authentication Attempts

text
๐Ÿ”ด NO PROTECTION:
- Unlimited login attempts
- Enables brute force and credential stuffing

๐ŸŸข PROTECTION:
- Account lockout after N attempts (5-10 typically)
- Exponential backoff on failures
- CAPTCHA after 3 failed attempts
- IP-based rate limiting
- Device fingerprinting
- Anomaly detection (unusual location/time)

๐Ÿ”ธ CWE-384: Session Fixation

python
# ๐Ÿ”ด VULNERABLE
def login(username, password):
    if authenticate(username, password):
        # Session ID stays the same before and after login
        session['user_id'] = get_user_id(username)
        return "Logged in"

# ๐ŸŸข SECURE
def login(username, password):
    if authenticate(username, password):
        # โœ… Regenerate session ID on login
        session.regenerate_id()
        session['user_id'] = get_user_id(username)
        return "Logged in"

๐Ÿ”ธ CWE-916: Use of Password Hash With Insufficient Computational Effort

text
๐Ÿ”ด BROKEN:
- MD5 (cracked, instant)
- SHA-1 (cracked, instant)
- SHA-256 without salt (rainbow tables)
- SHA-256 with salt but 1 iteration (fast GPU attack)

๐ŸŸข SECURE (in order of preference):
- Argon2id (OWASP recommended)
- scrypt (good)
- bcrypt (classic, still secure)
- PBKDF2 (acceptable, use high iteration count)

Minimum settings:
- Argon2id: m=19MB, t=2, p=1
- bcrypt: cost=12
- PBKDF2: 600,000 iterations (SHA-256)
  1. ๐Ÿ”‘ Password-Based Authentication: Done Right

The NIST 800-63B Modern Password Guidelines

Forget everything you knew about passwords. NIST updated their guidelines in 2017, and most sites still follow outdated advice.

text
โŒ OLD (and WRONG) ADVICE:
  - Force password changes every 90 days
  - Require 1 uppercase, 1 lowercase, 1 digit, 1 symbol
  - Minimum 8 characters
  - Prohibit sequential characters
  - Password hints

โœ… NEW (NIST 800-63B) GUIDELINES:
  - Minimum 8 characters (12+ for high-security)
  - Check against known breach databases
  - Allow all printable characters AND spaces
  - Allow long passphrases (up to 64 chars minimum)
  - NO periodic forced changes (change only on compromise)
  - NO password hints
  - NO knowledge-based security questions
  - NO SMS-based 2FA for high-security contexts

Why Forced Password Rotation Is HARMFUL

text
Research shows (and NIST confirms):

When forced to change passwords every 90 days, users:
  ๐Ÿ”ด Create weaker passwords (easier to remember new ones)
  ๐Ÿ”ด Use predictable patterns: Password1 โ†’ Password2 โ†’ Password3
  ๐Ÿ”ด Reuse passwords from personal accounts
  ๐Ÿ”ด Write passwords down (sticky notes under keyboards)
  ๐Ÿ”ด Use password managers less effectively

Result: SECURITY DECREASES, not increases.

The Password Lifecycle

Password Storage: The Right Way

python
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ด CATASTROPHIC โ€” Plaintext storage
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def register(username, password):
    db.execute(
        "INSERT INTO users (username, password) VALUES (?, ?)",
        (username, password)  # ๐Ÿ”ด PLAINTEXT!
    )

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ด BROKEN โ€” MD5 (cracked instantly)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
import hashlib
def register(username, password):
    hashed = hashlib.md5(password.encode()).hexdigest()  # ๐Ÿ”ด
    db.execute("INSERT INTO users VALUES (?, ?)", (username, hashed))

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ด STILL BROKEN โ€” SHA-256 without salt (rainbow tables)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def register(username, password):
    hashed = hashlib.sha256(password.encode()).hexdigest()  # ๐Ÿ”ด
    db.execute("INSERT INTO users VALUES (?, ?)", (username, hashed))

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŸก OKAY โ€” bcrypt (acceptable, but Argon2id is better)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
import bcrypt
def register(username, password):
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode(), salt)
    db.execute("INSERT INTO users VALUES (?, ?)", (username, hashed))

# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŸข BEST โ€” Argon2id (OWASP recommended)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError

# Create hasher with OWASP-recommended parameters
ph = PasswordHasher(
    time_cost=2,       # Iterations
    memory_cost=19456, # 19 MB
    parallelism=1,     # Threads
    hash_len=32,       # Hash output length
    salt_len=16        # Salt length
)

def register(username, password):
    # โœ… Validate password strength FIRST
    if not is_strong_password(password):
        raise ValueError("Password too weak")
    
    # โœ… Check against breach database
    if is_breached_password(password):
        raise ValueError("Password found in data breaches")
    
    # โœ… Hash with Argon2id
    hashed = ph.hash(password)
    
    # โœ… Store only the hash (contains salt + params)
    db.execute(
        "INSERT INTO users (username, password_hash) VALUES (?, ?)",
        (username, hashed)
    )

def login(username, password):
    user = db.query_one(
        "SELECT password_hash FROM users WHERE username = ?",
        (username,)
    )
    
    if not user:
        # โœ… Prevent timing attacks โ€” hash a dummy password
        ph.hash("dummy_password_for_timing")
        raise AuthenticationError("Invalid credentials")
    
    try:
        ph.verify(user['password_hash'], password)
    except (VerifyMismatchError, VerificationError):
        raise AuthenticationError("Invalid credentials")
    
    # โœ… Check if hash needs upgrading (if we increase parameters later)
    if ph.check_needs_rehash(user['password_hash']):
        new_hash = ph.hash(password)
        db.execute(
            "UPDATE users SET password_hash = ? WHERE username = ?",
            (new_hash, username)
        )
    
    return "Logged in"

Checking Against Breached Passwords

python
import hashlib
import requests

def is_breached_password(password: str) -> bool:
    """
    Check password against HaveIBeenPwned API.
    Uses k-anonymity: only sends first 5 chars of SHA-1 hash.
    The password itself NEVER leaves your server.
    """
    # Hash the password
    sha1_hash = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix = sha1_hash[:5]
    suffix = sha1_hash[5:]
    
    # Query HIBP API (returns all hashes with same prefix)
    response = requests.get(
        f'https://api.pwnedpasswords.com/range/{prefix}',
        timeout=5
    )
    
    if response.status_code != 200:
        # Fail secure โ€” if we can't check, assume it's breached
        return True
    
    # Check if our suffix is in the response
    for line in response.text.splitlines():
        hash_suffix, count = line.split(':')
        if hash_suffix == suffix:
            return True  # Found in breaches
    
    return False
  1. ๐Ÿ›ก๏ธ Multi-Factor Authentication (MFA)

MFA is the single most effective defense against account takeovers. Microsoft reports MFA blocks 99.9% of automated attacks.

The Three Factors

MFA Methods Ranked

MethodSecurityUsabilityPhishing ResistantNotes
FIDO2 / WebAuthn๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโœ… YESBest option โ€” hardware keys or biometric
Hardware tokens (YubiKey)๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโœ… YESGreat for high-security
TOTP apps (Authy, Google)๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโŒ NOGood, but phishable
Push notifications๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโŒ NOFatigue attacks possible
SMS codes๐ŸŸก๐ŸŸก๐ŸŸข๐ŸŸข๐ŸŸข๐ŸŸขโŒ NODeprecated by NIST
Email codes๐ŸŸก๐ŸŸข๐ŸŸข๐ŸŸขโŒ NOOnly slightly better than SMS
Security questions๐Ÿ”ด๐ŸŸข๐ŸŸขโŒ NONever use

Why SMS 2FA Is Bad (But Still Better Than Nothing)

text
๐Ÿ”ด SMS 2FA vulnerabilities:
  1. SIM swapping (social engineer carrier โ†’ steal phone number)
  2. SS7 attacks (telecom protocol flaws โ†’ intercept SMS)
  3. Phishing (user types code into fake site)
  4. Malware on phone (reads SMS)
  5. Phone theft

NIST deprecated SMS 2FA in 2016 for government/high-security use.

But: SMS 2FA still blocks 76% of automated attacks.
     Better than NO MFA. But WebAuthn blocks 100%.

Implementing TOTP (Time-based One-Time Password)

python
import pyotp
import qrcode
from io import BytesIO
import base64

def enable_mfa(user_id: str):
    """Enable TOTP MFA for a user"""
    # โœ… Generate a cryptographically random secret
    secret = pyotp.random_base32()
    
    # โœ… Store encrypted in database (NOT plaintext!)
    encrypted_secret = encrypt(secret, get_encryption_key())
    db.execute(
        "UPDATE users SET mfa_secret = ?, mfa_enabled = FALSE WHERE id = ?",
        (encrypted_secret, user_id)
    )
    
    # โœ… Generate provisioning URI
    user = get_user(user_id)
    totp = pyotp.TOTP(secret)
    uri = totp.provisioning_uri(
        name=user['email'],
        issuer_name='MySecureApp'
    )
    
    # โœ… Generate QR code
    qr = qrcode.make(uri)
    buffer = BytesIO()
    qr.save(buffer, format='PNG')
    qr_base64 = base64.b64encode(buffer.getvalue()).decode()
    
    # โœ… Also provide backup codes
    backup_codes = generate_backup_codes(10)
    store_backup_codes(user_id, backup_codes)
    
    return {
        'qr_code': f'data:image/png;base64,{qr_base64}',
        'secret': secret,  # Show once for manual entry
        'backup_codes': backup_codes
    }

def verify_mfa(user_id: str, token: str) -> bool:
    """Verify TOTP token"""
    user = db.query_one(
        "SELECT mfa_secret FROM users WHERE id = ? AND mfa_enabled = TRUE",
        (user_id,)
    )
    
    if not user:
        return False
    
    secret = decrypt(user['mfa_secret'], get_encryption_key())
    totp = pyotp.TOTP(secret)
    
    # โœ… Allow 1 window drift (30 seconds) for clock skew
    # โœ… Verify once โ€” prevents token reuse
    if totp.verify(token, valid_window=1):
        # Check for replay attack
        if is_token_already_used(user_id, token):
            return False
        mark_token_used(user_id, token)
        return True
    
    return False

def generate_backup_codes(count: int = 10) -> list:
    """Generate single-use backup codes"""
    import secrets
    import string
    
    codes = []
    for _ in range(count):
        # Format: XXXX-XXXX (readable)
        code = '-'.join(
            ''.join(secrets.choice(string.ascii_uppercase + string.digits)
                   for _ in range(4))
            for _ in range(2)
        )
        codes.append(code)
    
    return codes

WebAuthn / FIDO2: The Future of Authentication

javascript
// ๐ŸŸข WEBAUTHN REGISTRATION (Browser)

async function registerPasskey() {
    // Server generates challenge
    const options = await fetch('/api/webauthn/register-options', {
        method: 'POST'
    }).then(r => r.json());
    
    // Browser creates credential (uses platform authenticator or security key)
    const credential = await navigator.credentials.create({
        publicKey: {
            challenge: base64ToBuffer(options.challenge),
            rp: {
                name: 'MySecureApp',
                id: 'example.com'
            },
            user: {
                id: base64ToBuffer(options.user.id),
                name: options.user.email,
                displayName: options.user.name
            },
            pubKeyCredParams: [
                { alg: -7, type: 'public-key' },   // ES256
                { alg: -257, type: 'public-key' }  // RS256
            ],
            authenticatorSelection: {
                authenticatorAttachment: 'platform',  // Prefer built-in
                userVerification: 'required',         // Require biometric/PIN
                residentKey: 'preferred'              // Discoverable credentials
            },
            timeout: 60000,
            attestation: 'direct'
        }
    });
    
    // Send credential to server for verification
    await fetch('/api/webauthn/register', {
        method: 'POST',
        body: JSON.stringify({
            id: credential.id,
            rawId: bufferToBase64(credential.rawId),
            response: {
                clientDataJSON: bufferToBase64(credential.response.clientDataJSON),
                attestationObject: bufferToBase64(credential.response.attestationObject)
            }
        })
    });
}

Why WebAuthn is revolutionary:

  • ๐ŸŸข Phishing-resistant โ€” Tied to the domain, can't be used on fake sites
  • ๐ŸŸข No shared secrets โ€” Private key never leaves device
  • ๐ŸŸข Biometric-friendly โ€” Uses Touch ID, Face ID, Windows Hello
  • ๐ŸŸข No codes to steal โ€” Nothing to type, nothing to intercept
  • ๐ŸŸข Resistant to replay โ€” Each authentication is unique
  1. ๐Ÿช Session Management: The Forgotten Battlefield

Even perfect authentication is useless if your session management is broken.

The Session Lifecycle

Secure Cookie Configuration

http
"text-slate-500 font-normal italic"># ๐ŸŸข PRODUCTION-READY SESSION COOKIE

Set-Cookie: __Host-sessionid=abc123xyz...;
    Secure;                        "text-slate-500 font-normal italic"># Only sent over HTTPS
    HttpOnly;                      "text-slate-500 font-normal italic"># No JavaScript access (XSS protection)
    SameSite=Strict;              "text-slate-500 font-normal italic"># No cross-site requests (CSRF protection)
    Path=/;                        "text-slate-500 font-normal italic"># Available site-wide
    Max-Age=3600;                 "text-slate-500 font-normal italic"># Expires in 1 hour
    "text-slate-500 font-normal italic"># No Domain attribute (default: current host only)

"text-slate-500 font-normal italic"># Why __Host- prefix?
"text-slate-500 font-normal italic"># - Requires Secure flag
"text-slate-500 font-normal italic"># - Prohibits Domain attribute
"text-slate-500 font-normal italic"># - Requires Path=/
"text-slate-500 font-normal italic"># - Maximum cookie security

Session Security Checklist

python
# ๐ŸŸข SECURE SESSION CONFIGURATION

from flask import Flask
from datetime import timedelta
import secrets

app = Flask(__name__)

app.config.update(
    # โœ… Strong secret key (never hardcode!)
    SECRET_KEY=os.environ['SECRET_KEY'],  # 64+ random bytes
    
    # โœ… Secure cookie settings
    SESSION_COOKIE_NAME='__Host-session',
    SESSION_COOKIE_SECURE=True,        # HTTPS only
    SESSION_COOKIE_HTTPONLY=True,      # No JS access
    SESSION_COOKIE_SAMESITE='Strict',  # CSRF protection
    
    # โœ… Short expiration
    PERMANENT_SESSION_LIFETIME=timedelta(minutes=30),
    
    # โœ… Session regeneration
    SESSION_REFRESH_EACH_REQUEST=True,
)

def generate_session_id() -> str:
    """Generate cryptographically secure session ID"""
    # โœ… 256 bits of entropy minimum
    return secrets.token_urlsafe(32)

def login(username, password, mfa_token):
    # โœ… Verify credentials
    user = authenticate(username, password, mfa_token)
    if not user:
        raise AuthenticationError()
    
    # โœ… CRITICAL: Regenerate session ID on login
    # Prevents session fixation attacks
    session.clear()
    session.regenerate()  # New ID
    
    # โœ… Store minimal data in session
    session['user_id'] = user.id
    session['authenticated_at'] = time.time()
    session['ip_address'] = request.remote_addr
    session['user_agent_hash'] = hash_user_agent(request.user_agent)
    
    # โœ… Store session server-side (not just in cookie)
    session_store.set(session.sid, {
        'user_id': user.id,
        'created_at': datetime.utcnow(),
        'last_activity': datetime.utcnow(),
        'ip_address': request.remote_addr
    }, ex=1800)  # 30 min TTL
    
    # โœ… Log authentication event
    audit_log.info(f"Login success: user={user.id} ip={request.remote_addr}")

def validate_session():
    """Validate session on every request"""
    if 'user_id' not in session:
        return None
    
    # โœ… Check session age
    if time.time() - session['authenticated_at'] > 86400:  # 24 hours max
        session.clear()
        return None
    
    # โœ… Check idle timeout (30 min)
    last_activity = session.get('last_activity', 0)
    if time.time() - last_activity > 1800:
        session.clear()
        return None
    
    # โœ… Detect session hijacking
    # Check if IP or User-Agent changed (session binding)
    if session.get('ip_address') != request.remote_addr:
        audit_log.warning(f"Session IP mismatch: user={session['user_id']}")
        session.clear()
        return None
    
    if session.get('user_agent_hash') != hash_user_agent(request.user_agent):
        audit_log.warning(f"User-Agent change: user={session['user_id']}")
        session.clear()
        return None
    
    # โœ… Update last activity
    session['last_activity'] = time.time()
    
    return get_user(session['user_id'])

def logout():
    user_id = session.get('user_id')
    
    # โœ… Delete session server-side
    if hasattr(session, 'sid'):
        session_store.delete(session.sid)
    
    # โœ… Clear client-side cookie
    session.clear()
    
    # โœ… Audit log
    if user_id:
        audit_log.info(f"Logout: user={user_id}")

Session Timeout Strategy

text
๐ŸŸข LAYERED TIMEOUTS:

1. Idle Timeout:        15-30 minutes
   โ†’ User inactive = logged out
   โ†’ Prevents unattended session abuse

2. Absolute Timeout:    8-24 hours
   โ†’ Maximum session lifetime
   โ†’ Forces re-authentication regardless of activity

3. Re-authentication:   For sensitive actions
   โ†’ Change password, transfer money, delete data
   โ†’ Require password/MFA even in active session

4. Session Invalidation on:
   - Password change
   - MFA setup/removal
   - Email change
   - Suspicious activity
   - Logout from any device (if "logout all" chosen)
  1. ๐ŸŽซ JWT Security: The Modern Pitfalls

JSON Web Tokens (JWTs) are everywhere in modern APIs. They're also routinely implemented incorrectly.

JWT Structure

text
header.payload.signature

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NSJ9.X_signature

Header (base64):   {"alg": "HS256", "typ": "JWT"}
Payload (base64):  {"sub": "12345", "exp": 1234567890}
Signature:         HMAC-SHA256(header + "." + payload, secret)

The "None" Algorithm Attack

python
# ๐Ÿ”ด CRITICAL VULNERABILITY โ€” "none" algorithm bypass

# Attacker sends:
# Header:  {"alg": "none"}
# Payload: {"sub": "admin", "role": "superuser"}
# Signature: (empty)

# ๐Ÿ”ด VULNERABLE CODE
import jwt
def verify_token(token):
    payload = jwt.decode(token, verify=False)  # ๐Ÿ”ด DISASTER
    return payload

# ๐ŸŸข SECURE CODE
def verify_token(token):
    try:
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=['HS256'],  # โœ… Explicitly specify algorithm
            options={
                'verify_signature': True,
                'verify_exp': True,
                'verify_iat': True,
                'verify_nbf': True,
                'require': ['exp', 'iat', 'sub']
            }
        )
        return payload
    except jwt.InvalidTokenError:
        return None

The Algorithm Confusion Attack

text
Scenario: Server uses RS256 (asymmetric)
          Attacker tricks server to use HS256 (symmetric)

1. Attacker gets server's PUBLIC key (it's public!)
2. Attacker crafts token:
   - Header: {"alg": "HS256"}  (changed from RS256)
   - Payload: {"sub": "admin"}
   - Signature: HMAC-SHA256(header + payload, PUBLIC_KEY)

3. Server receives token, uses PUBLIC key to verify HS256
4. Signature matches! โ†’ Attacker is admin

๐ŸŸข FIX: Hard-code expected algorithm, reject tokens with mismatched alg

Secure JWT Implementation

python
import jwt
import datetime
import secrets
from functools import wraps

# โœ… Use STRONG secret (256+ bits for HS256)
JWT_SECRET = os.environ['JWT_SECRET']  # Loaded from secrets manager
JWT_ALGORITHM = 'HS256'
JWT_ISSUER = 'myapp.com'
JWT_AUDIENCE = 'myapp.com/api'

# โœ… Short access token lifetime
ACCESS_TOKEN_EXPIRE_MINUTES = 15

# โœ… Longer refresh token lifetime (but still limited)
REFRESH_TOKEN_EXPIRE_DAYS = 7

def create_access_token(user_id: str, roles: list) -> str:
    """Create short-lived access token"""
    now = datetime.datetime.utcnow()
    
    payload = {
        'sub': user_id,                    # Subject (user ID)
        'iat': now,                        # Issued at
        'exp': now + datetime.timedelta(   # Expiration
            minutes=ACCESS_TOKEN_EXPIRE_MINUTES
        ),
        'nbf': now,                        # Not before
        'iss': JWT_ISSUER,                 # Issuer
        'aud': JWT_AUDIENCE,               # Audience
        'jti': secrets.token_urlsafe(16),  # Unique token ID
        'type': 'access',
        'roles': roles
    }
    
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def create_refresh_token(user_id: str) -> str:
    """Create refresh token (stored server-side)"""
    now = datetime.datetime.utcnow()
    token_id = secrets.token_urlsafe(32)
    
    payload = {
        'sub': user_id,
        'iat': now,
        'exp': now + datetime.timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS),
        'nbf': now,
        'iss': JWT_ISSUER,
        'jti': token_id,
        'type': 'refresh'
    }
    
    # โœ… Store refresh token server-side for revocation
    refresh_store.set(f"refresh:{token_id}", {
        'user_id': user_id,
        'created_at': now.isoformat()
    }, ex=REFRESH_TOKEN_EXPIRE_DAYS * 86400)
    
    return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)

def verify_token(token: str, token_type: str = 'access') -> dict:
    """Verify and decode JWT"""
    try:
        payload = jwt.decode(
            token,
            JWT_SECRET,
            algorithms=[JWT_ALGORITHM],  # โœ… Explicit algorithm
            issuer=JWT_ISSUER,
            audience=JWT_AUDIENCE,
            options={
                'verify_signature': True,
                'verify_exp': True,
                'verify_iat': True,
                'verify_nbf': True,
                'verify_iss': True,
                'verify_aud': True,
                'require': ['exp', 'iat', 'sub', 'jti', 'type']
            }
        )
        
        # โœ… Verify token type
        if payload.get('type') != token_type:
            raise jwt.InvalidTokenError("Wrong token type")
        
        # โœ… Check revocation list
        if is_token_revoked(payload['jti']):
            raise jwt.InvalidTokenError("Token revoked")
        
        # โœ… For refresh tokens, verify server-side existence
        if token_type == 'refresh':
            if not refresh_store.exists(f"refresh:{payload['jti']}"):
                raise jwt.InvalidTokenError("Refresh token not found")
        
        return payload
        
    except jwt.ExpiredSignatureError:
        raise AuthenticationError("Token expired")
    except jwt.InvalidTokenError as e:
        raise AuthenticationError(f"Invalid token: {e}")

def revoke_token(jti: str, exp: datetime.datetime):
    """Add token to revocation list"""
    ttl = int((exp - datetime.datetime.utcnow()).total_seconds())
    if ttl > 0:
        revocation_store.set(f"revoked:{jti}", "1", ex=ttl)

def is_token_revoked(jti: str) -> bool:
    return revocation_store.exists(f"revoked:{jti}")

JWT Best Practices Summary

text
โœ… DO:
  - Use short expiration (15 min for access tokens)
  - Implement refresh token rotation
  - Store refresh tokens server-side for revocation
  - Use strong secrets (256+ bits)
  - Explicitly specify algorithm when verifying
  - Validate ALL claims (exp, iat, iss, aud, nbf)
  - Use HTTPS exclusively
  - Store in httpOnly, Secure, SameSite cookies (not localStorage)

โŒ DON'T:
  - Store JWTs in localStorage (XSS vulnerability)
  - Use "none" algorithm
  - Trust the alg header without validation
  - Use long-lived access tokens
  - Store sensitive data in payload (it's base64, not encrypted)
  - Skip signature verification "for performance"
  - Use JWTs for session management without revocation strategy
  1. ๐ŸŒ OAuth 2.0 & OIDC: Doing It Right

OAuth 2.0 is authorization. OpenID Connect (OIDC) is authentication built on OAuth. Both are commonly misused.

The OAuth 2.0 Flows

Authorization Code Flow + PKCE (Modern Standard)

Common OAuth Pitfalls

text
๐Ÿ”ด PITFALL 1: Using Implicit Flow in SPAs
   OLD: Implicit flow returned tokens in URL fragment (leaked)
   NEW: Use Authorization Code + PKCE for SPAs

๐Ÿ”ด PITFALL 2: Not validating redirect_uri
   Attacker: /authorize?redirect_uri=attacker.com
   Server: Sends code to attacker.com
   FIX: Exact match whitelist of redirect URIs

๐Ÿ”ด PITFALL 3: Using OAuth for authentication (without OIDC)
   OAuth = "This app can access your data"
   NOT = "This person is who they say they are"
   FIX: Use OIDC (which adds id_token for authentication)

๐Ÿ”ด PITFALL 4: Not verifying state parameter (CSRF)
   FIX: Generate random state, verify on callback

๐Ÿ”ด PITFALL 5: Treating access tokens as session tokens
   Access tokens = short-lived (minutes)
   Sessions = can be longer with proper management
   
๐Ÿ”ด PITFALL 6: Storing tokens in localStorage
   XSS = instant token theft
   FIX: httpOnly cookies or BFF (Backend for Frontend) pattern
  1. ๐Ÿ”ฎ Passwordless Authentication: The Future

The password is dying. Here's what's replacing it:

Passkeys (FIDO2 + WebAuthn)

text
๐Ÿ”‘ How Passkeys Work:

1. User registers: Device generates public/private key pair
2. Private key: NEVER leaves the device (secured by biometric/PIN)
3. Public key: Sent to server, stored with user account
4. Login: Server sends challenge โ†’ device signs with private key
5. Server verifies signature with public key โ†’ authenticated

Benefits:
  ๐ŸŸข Phishing-impossible (tied to domain cryptographically)
  ๐ŸŸข No shared secret to steal
  ๐ŸŸข Works across devices (iCloud/Google sync)
  ๐ŸŸข Biometric unlock (Face ID, Touch ID, Windows Hello)
  ๐ŸŸข Faster than passwords

Magic Links

python
# โœ… MAGIC LINK AUTHENTICATION

import secrets
from datetime import datetime, timedelta

def send_magic_link(email: str):
    # โœ… Rate limit
    if is_rate_limited(email):
        return  # Don't reveal if user exists
    
    # โœ… Generate cryptographically random token
    token = secrets.token_urlsafe(32)
    
    # โœ… Store with short TTL (10-15 minutes)
    magic_link_store.set(
        f"magic:{token}",
        {'email': email, 'created_at': datetime.utcnow().isoformat()},
        ex=900  # 15 minutes
    )
    
    # โœ… Send email with link
    link = f"https://app.example.com/auth/verify?token={token}"
    send_email(
        to=email,
        subject="Your login link",
        body=f"Click to log in: {link}\n\nExpires in 15 minutes."
    )

def verify_magic_link(token: str):
    data = magic_link_store.get(f"magic:{token}")
    if not data:
        raise AuthenticationError("Invalid or expired link")
    
    # โœ… Delete immediately (single-use)
    magic_link_store.delete(f"magic:{token}")
    
    # โœ… Create session
    user = get_or_create_user(data['email'])
    create_session(user.id)
    
    return user
  1. ๐Ÿ”ง Account Recovery: The Weakest Link

The security of an account is the security of its recovery process. Attackers often skip the login page and go straight to "Forgot Password."

The Recovery Hierarchy (Best to Worst)

Secure Password Reset Flow

python
def request_password_reset(email: str):
    """Initiate password reset"""
    # โœ… Rate limit (per email AND per IP)
    if is_rate_limited(f"reset:{email}") or is_rate_limited(f"ip:{request.ip}"):
        # Silent fail โ€” don't reveal rate limiting
        return
    
    user = get_user_by_email(email)
    
    # โœ… ALWAYS respond the same way (prevent enumeration)
    # Even if user doesn't exist, pretend we sent email
    response_delay()  # Consistent timing
    
    if user:
        # โœ… Generate cryptographically secure token
        token = secrets.token_urlsafe(32)
        token_hash = hashlib.sha256(token.encode()).hexdigest()
        
        # โœ… Store HASH of token (not plaintext)
        db.execute("""
            INSERT INTO password_resets (user_id, token_hash, expires_at)
            VALUES (?, ?, ?)
        """, (user.id, token_hash, datetime.utcnow() + timedelta(minutes=15)))
        
        # โœ… Send email with link
        link = f"https://app.com/reset?token={token}"
        send_email(
            to=email,
            subject="Reset your password",
            body=f"Click to reset: {link}\n\n"
                 f"Expires in 15 minutes.\n"
                 f"If you didn't request this, ignore this email.\n"
                 f"Request from IP: {request.ip} at {datetime.utcnow()}"
        )
        
        # โœ… Notify user on ALL channels
        if user.phone:
            send_sms(user.phone, "Password reset requested. Not you? Reply STOP.")
        
        # โœ… Audit log
        audit_log.info(f"Password reset requested: user={user.id}")

def verify_reset_token(token: str, new_password: str):
    """Complete password reset"""
    token_hash = hashlib.sha256(token.encode()).hexdigest()
    
    # โœ… Look up by hash (constant time comparison)
    reset = db.query_one("""
        SELECT user_id, expires_at FROM password_resets
        WHERE token_hash = ? AND used_at IS NULL
    """, (token_hash,))
    
    if not reset:
        raise ValueError("Invalid or expired token")
    
    if reset.expires_at < datetime.utcnow():
        raise ValueError("Token expired")
    
    # โœ… Validate new password
    if not is_strong_password(new_password):
        raise ValueError("Password too weak")
    
    if is_breached_password(new_password):
        raise ValueError("Password found in breaches")
    
    user = get_user(reset.user_id)
    
    # โœ… Check it's not the same as current password
    if verify_password(new_password, user.password_hash):
        raise ValueError("Must be different from current password")
    
    # โœ… Update password
    user.password_hash = hash_password(new_password)
    db.save(user)
    
    # โœ… Mark token as used (single-use)
    db.execute(
        "UPDATE password_resets SET used_at = ? WHERE token_hash = ?",
        (datetime.utcnow(), token_hash)
    )
    
    # โœ… Invalidate ALL existing sessions (force re-login everywhere)
    invalidate_all_sessions(user.id)
    
    # โœ… Notify user of change
    send_email(
        to=user.email,
        subject="Your password was changed",
        body=f"Your password was changed at {datetime.utcnow()}.\n"
             f"If this wasn't you, contact support IMMEDIATELY.\n"
             f"All active sessions have been terminated."
    )
    
    # โœ… Require MFA re-enrollment if MFA was reset
    if was_mfa_bypassed_during_recovery:
        require_mfa_setup(user.id)
    
    audit_log.info(f"Password reset completed: user={user.id}")
  1. ๐Ÿ’ป Code Examples: Vulnerable vs Secure

Example 1: Complete Login Implementation

python
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐Ÿ”ด VULNERABLE LOGIN
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

@app.route('/login', methods=['POST'])
def login():
    username = request.form.get('username')
    password = request.form.get('password')
    
    # ๐Ÿ”ด SQL injection + plaintext comparison
    user = db.execute(
        f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    ).fetchone()
    
    if user:
        session['user'] = username  # ๐Ÿ”ด No session regeneration
        return "Logged in"
    else:
        # ๐Ÿ”ด Different messages = enumeration
        if db.execute(f"SELECT * FROM users WHERE username='{username}'").fetchone():
            return "Wrong password"
        else:
            return "User not found"


# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๐ŸŸข SECURE LOGIN
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

from flask import Flask, request, session, abort
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
import secrets
import time
from datetime import datetime, timedelta

limiter = Limiter(app, key_func=get_remote_address)
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)

@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")      # โœ… IP rate limit
@limiter.limit("10 per hour")       # โœ… Hourly limit
def login():
    data = request.get_json()
    username = data.get('username', '').strip().lower()
    password = data.get('password', '')
    mfa_token = data.get('mfa_token')
    
    # โœ… Input validation
    if not username or not password:
        return {'error': 'Credentials required'}, 400
    
    if len(username) > 254 or len(password) > 128:
        return {'error': 'Invalid credentials'}, 401
    
    # โœ… Check if account is locked
    if is_account_locked(username):
        audit_log.warning(f"Login attempt on locked account: {username}")
        # Same response as invalid credentials
        time.sleep(0.5)  # Consistent timing
        return {'error': 'Invalid credentials'}, 401
    
    # โœ… Check if IP is blocked
    if is_ip_blocked(request.remote_addr):
        audit_log.warning(f"Login from blocked IP: {request.remote_addr}")
        abort(403)
    
    # โœ… Parameterized query
    user = db.query_one(
        "SELECT id, password_hash, mfa_enabled, mfa_secret, failed_attempts "
        "FROM users WHERE username = ? AND is_active = TRUE",
        (username,)
    )
    
    # โœ… Timing attack prevention
    # Always perform hash verification, even if user doesn't exist
    if not user:
        # Dummy verification to maintain consistent timing
        try:
            ph.verify(
                "$argon2id$v=19$m=19456,t=2,p=1$dummysalt$dummyhashvalue",
                password
            )
        except:
            pass
        
        time.sleep(0.1)  # Add jitter
        audit_log.info(f"Login failed (no user): {username} from {request.remote_addr}")
        return {'error': 'Invalid credentials'}, 401
    
    # โœ… Verify password
    try:
        ph.verify(user['password_hash'], password)
    except VerifyMismatchError:
        # โœ… Increment failed attempts
        increment_failed_attempts(user['id'])
        
        # โœ… Lock account after 5 failed attempts
        if user['failed_attempts'] + 1 >= 5:
            lock_account(user['id'], duration_minutes=30)
            send_security_alert(user['id'], 'account_locked')
        
        audit_log.info(f"Login failed (bad password): user={user['id']}")
        return {'error': 'Invalid credentials'}, 401
    
    # โœ… Password correct โ€” check if MFA required
    if user['mfa_enabled']:
        if not mfa_token:
            # Return partial authentication (needs MFA)
            temp_token = create_mfa_pending_token(user['id'])
            return {'mfa_required': True, 'mfa_token_id': temp_token}, 200
        
        if not verify_totp(user['mfa_secret'], mfa_token):
            audit_log.warning(f"MFA failed: user={user['id']}")
            return {'error': 'Invalid credentials'}, 401
    
    # โœ… Successful login
    reset_failed_attempts(user['id'])
    
    # โœ… Check for suspicious login (new location/device)
    if is_suspicious_login(user['id'], request):
        send_security_alert(user['id'], 'new_device_login')
    
    # โœ… Rehash if parameters have changed
    if ph.check_needs_rehash(user['password_hash']):
        new_hash = ph.hash(password)
        db.execute(
            "UPDATE users SET password_hash = ? WHERE id = ?",
            (new_hash, user['id'])
        )
    
    # โœ… Regenerate session ID (prevent fixation)
    session.clear()
    session.regenerate()
    
    # โœ… Set session data
    session['user_id'] = user['id']
    session['authenticated_at'] = time.time()
    session['ip'] = request.remote_addr
    session['ua_hash'] = hash_user_agent(request.headers.get('User-Agent'))
    session.permanent = True
    
    # โœ… Audit log
    audit_log.info(
        f"Login success: user={user['id']} "
        f"ip={request.remote_addr}"
    )
    
    # โœ… Update last login
    db.execute(
        "UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?",
        (datetime.utcnow(), request.remote_addr, user['id'])
    )
    
    return {'success': True, 'user': {'id': user['id']}}, 200
  1. โœ… The Ultimate Checklist

๐Ÿ” Authentication Security Checklist

๐Ÿ”‘ Password Management

  • [ ] Minimum 12 characters required (NIST recommends 8+, prefer longer)
  • [ ] Allow all printable characters including spaces
  • [ ] Allow passphrases up to 64+ characters
  • [ ] Check against breached password databases (HaveIBeenPwned)
  • [ ] NO periodic forced password changes (unless breach suspected)
  • [ ] NO arbitrary complexity rules (per NIST guidelines)
  • [ ] NO password hints
  • [ ] NO security questions
  • [ ] Passwords hashed with Argon2id (preferred), scrypt, or bcrypt
  • [ ] Hash parameters meet OWASP recommendations
  • [ ] Salt generated per-password with crypto-random
  • [ ] Automatic rehashing when parameters upgraded

๐Ÿ›ก๏ธ Multi-Factor Authentication

  • [ ] MFA available for all users
  • [ ] MFA REQUIRED for admin accounts
  • [ ] MFA REQUIRED for sensitive operations
  • [ ] FIDO2/WebAuthn supported (preferred method)
  • [ ] TOTP (Authy, Google Authenticator) supported
  • [ ] Backup codes generated (single-use)
  • [ ] SMS 2FA avoided (or marked as weaker option)
  • [ ] MFA cannot be bypassed via account recovery
  • [ ] MFA setup/removal requires re-authentication
  • [ ] User notified when MFA settings changed

๐Ÿšซ Brute Force Protection

  • [ ] Account lockout after 5-10 failed attempts
  • [ ] Progressive delays between attempts
  • [ ] CAPTCHA after 3 failed attempts
  • [ ] IP-based rate limiting
  • [ ] User-based rate limiting
  • [ ] Password spraying detection
  • [ ] Anomaly detection (unusual location/time/device)
  • [ ] Alerts sent to user on suspicious activity
  • [ ] Honeypot accounts to detect attacks

๐Ÿช Session Management

  • [ ] Session IDs: 256+ bits of entropy
  • [ ] Session IDs regenerated on login
  • [ ] Session IDs regenerated on privilege change
  • [ ] Idle timeout: 15-30 minutes
  • [ ] Absolute timeout: 8-24 hours max
  • [ ] Session cookies: Secure, HttpOnly, SameSite=Strict
  • [ ] Sessions stored server-side (not just in cookie)
  • [ ] Sessions invalidated on logout
  • [ ] Sessions invalidated on password change
  • [ ] Session binding (IP, User-Agent) with care for mobile users
  • [ ] "Logout everywhere" functionality
  • [ ] Active sessions visible to user

๐ŸŽซ Token Security (JWT/OAuth)

  • [ ] JWT algorithm explicitly specified on verification
  • [ ] "none" algorithm rejected
  • [ ] Short access token lifetime (15 min)
  • [ ] Refresh tokens with rotation
  • [ ] Refresh tokens revocable server-side
  • [ ] Tokens stored in httpOnly cookies (not localStorage)
  • [ ] All JWT claims validated (exp, iat, iss, aud, nbf)
  • [ ] OAuth: Authorization Code + PKCE (not Implicit)
  • [ ] OAuth: Exact-match redirect URI validation
  • [ ] OAuth: State parameter for CSRF protection
  • [ ] OIDC used for authentication (not raw OAuth)

๐Ÿ”„ Account Recovery

  • [ ] Recovery process is as strong as authentication
  • [ ] Time-limited recovery tokens (15 min)
  • [ ] Single-use recovery tokens
  • [ ] Recovery tokens stored as hashes
  • [ ] Rate limiting on recovery requests
  • [ ] Same response regardless of email existence
  • [ ] Multi-channel notification on recovery
  • [ ] MFA cannot be fully bypassed via recovery
  • [ ] All sessions invalidated after password reset
  • [ ] Audit trail of all recovery events

๐Ÿ“ Audit & Monitoring

  • [ ] All authentication events logged
  • [ ] Failed login attempts logged
  • [ ] Suspicious patterns detected automatically
  • [ ] Account lockouts tracked
  • [ ] MFA changes audited
  • [ ] Password changes audited
  • [ ] New device/location notifications
  • [ ] Real-time alerting on compromise indicators
  • [ ] Login history visible to user
  1. ๐Ÿ Conclusion

The Hard Truth

Key Takeaways

๐Ÿ”ด Passwords alone are not enough. Period.
MFA blocks 99.9% of automated attacks. If you don't offer MFA, you don't take security seriously.
๐ŸŸก Sessions are as important as passwords.
A compromised session = compromised account. Treat session tokens like gold.
๐ŸŸข Modern authentication is passwordless.
WebAuthn/Passkeys are phishing-impossible. The future is here.
๐Ÿ”ต Recovery is often weaker than login.
If "Forgot Password" doesn't require MFA, MFA is useless.
๐ŸŸฃ Credentials in a breach are now PUBLIC.
Check against HaveIBeenPwned. Reject compromised passwords.

The Developer's Oath

text
I will hash passwords with Argon2id, not MD5.
I will require MFA for all privileged accounts.
I will regenerate session IDs on login.
I will rate limit every authentication endpoint.
I will give consistent responses to prevent enumeration.
I will secure cookies with HttpOnly, Secure, and SameSite.
I will treat JWT like plaintext โ€” because the payload IS plaintext.
I will offer passkey-based authentication.
I will make recovery as secure as login.
I will audit every authentication event.

The Cost of Inaction

text
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              THE COST OF AUTHENTICATION FAILURES                   โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                    โ”‚
โ”‚  Colonial Pipeline (1 missing MFA):          $4.4M + national crisisโ”‚
โ”‚  23andMe (credential stuffing):              $30M + 6.9M victims   โ”‚
โ”‚  Twitter 2020 (admin takeover):              $120K scam + chaos    โ”‚
โ”‚  Average account takeover cost:              $1,200 per account    โ”‚
โ”‚  Average breach involving credentials:       $4.5M total           โ”‚
โ”‚                                                                    โ”‚
โ”‚  Cost of implementing MFA:                   ~$0 (free providers)  โ”‚
โ”‚  Cost of implementing WebAuthn:              ~$0 (open standard)   โ”‚
โ”‚  Cost of using Argon2id:                     ~$0 (library)         โ”‚
โ”‚                                                                    โ”‚
โ”‚  ROI of secure authentication:               INFINITE              โ”‚
โ”‚                                                                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Final Words

Authentication is the foundation. If it fails, nothing else matters. The best encryption, the best firewall, the best code โ€” all worthless if an attacker can simply log in as a legitimate user.

In 2024, there is no excuse for:

  • โŒ Storing passwords in plaintext or MD5
  • โŒ Not offering MFA
  • โŒ Using SMS as the only MFA option
  • โŒ Allowing credential stuffing without rate limiting
  • โŒ Not checking against breach databases
  • โŒ Session fixation vulnerabilities
  • โŒ JWT "none" algorithm acceptance
  • โŒ Security questions as account recovery

Build authentication like your business depends on it. Because it does.

Every login is a battle. Every session is a target. Every password reset is an opportunity for attack.

Make your authentication a fortress, not a front door with a sticky note that says "key under mat."

<div align="center">

๐Ÿ“š Further Reading

ResourceURL
OWASP Top 10 โ€” 2021https://owasp.org/Top10/
OWASP Authentication Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
OWASP Session Management Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
OWASP Password Storage Cheat Sheethttps://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
NIST SP 800-63B Digital Identity Guidelineshttps://pages.nist.gov/800-63-3/sp800-63b.html
HaveIBeenPwned APIhttps://haveibeenpwned.com/API/v3
FIDO Alliance (WebAuthn)https://fidoalliance.org/
WebAuthn.io (Demo)https://webauthn.io/
OAuth 2.0 Security Best Current Practicehttps://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
JWT Best Practices (RFC 8725)https://datatracker.ietf.org/doc/html/rfc8725

Category: Application Security ยท Authentication ยท Identity Management ยท OWASP Tags: #OWASP #Authentication #MFA #WebAuthn #JWT #OAuth #Passkeys #A07 #SessionManagement

"The user's password is the key to their digital life. Treat it with the respect it deserves โ€” and build systems that don't rely on it alone."
"MFA is not optional. It's not advanced. It's not enterprise-grade. It's the bare minimum." โ€” Modern Security Reality
"The best password is one you don't have to remember โ€” because it doesn't exist." โ€” The Passkey Era

</div>