Security Advanced

Web Authentication Attacks — Sessions, JWT, OAuth, SSO, Account Takeover

How authentication actually breaks in modern web apps — session theft, JWT confusion, OAuth flaws, SSO race conditions, and the controls that actually work.

DjangoZen Team May 10, 2026 14 min read 2 views

Why auth is special

Auth bugs convert directly to compromise. A SQLi reveals data. An XSS phishes users. An auth flaw lets the attacker BE another user — usually with admin somewhere in the chain. That's why auth deserves dedicated attention beyond the OWASP categories.

This tutorial covers the major auth attack patterns and the defenses that hold under real adversarial pressure.

Session management — what goes wrong

Even mature frameworks have edge cases.

Session fixation

The classic: attacker sets a session ID before the user logs in (via a malicious link, XSS, or just guessing). User logs in. Now attacker knows a valid post-auth session ID.

Defense: rotate the session ID on every privilege change (login, MFA verification, password change). Django does this in login() — don't override.

# Wrong (Django < 1.7 era; mistakes still happen)
request.session['user_id'] = user.id  # ID doesn't rotate

# Right (Django's auth.login)
from django.contrib.auth import login
login(request, user)  # Rotates session ID

Session prediction

Older systems used predictable session IDs (sequential, timestamp-based, weak randomness). Modern frameworks use cryptographically secure random IDs. Verify yours does — it's the difference between brute-forceable and not.

Cross-context session reuse

Same session ID accepted in different contexts (web + mobile + API). If one context is compromised, all are. Modern pattern: separate tokens per context, or bind tokens to specific clients via TLS fingerprints.

Long-lived sessions

A session that lasts months is a months-long window of compromise. After credential theft (via malware on user device), attacker has continuous access.

Defenses:

  • Short session age — 4 to 24 hours typical for sensitive apps
  • Absolute and idle timeouts both — even active users get re-auth periodically
  • Logout invalidates server-side, not just clears cookies
  • Token rotation within a session — issue new ID periodically, old one becomes invalid
  • Step-up auth for sensitive actions — re-enter password before changing email

Cross-device session listing

Users should see all their active sessions and revoke individually. Critical when their device is stolen and password change requires email access they can't get.

# Show user their active sessions
class UserSession(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    session_key = models.CharField(max_length=40)
    ip_address = models.GenericIPAddressField()
    user_agent = models.CharField(max_length=512)
    created_at = models.DateTimeField(auto_now_add=True)
    last_active = models.DateTimeField(auto_now=True)
    location = models.CharField(max_length=100, blank=True)

    def revoke(self):
        from django.contrib.sessions.models import Session
        try:
            Session.objects.get(session_key=self.session_key).delete()
        except Session.DoesNotExist:
            pass
        self.delete()

JWT — the popular but error-prone token

JSON Web Tokens are everywhere in modern auth. They're also one of the most consistently misimplemented protocols.

The structure

header.payload.signature

Each part is base64-encoded JSON. The signature is over the header + payload using a key (HMAC for symmetric, RSA/ECDSA for asymmetric).

Classic JWT bugs

alg: none — the JWT spec allows "alg": "none" for tokens with no signature. Some libraries accept these even when expecting signed tokens. Result: attacker crafts a JWT with any payload, no signature, library accepts it.

// Vulnerable verification
jwt.verify(token, key);  // Defaults may accept alg:none in old libraries

// Safe verification — always specify expected algorithms
jwt.verify(token, key, { algorithms: ['HS256'] });

Algorithm confusion (RS256 vs HS256) — server signs with RS256 (asymmetric); attacker takes the public key, signs a JWT with HS256 using the public key as the HMAC secret. If the server's verify accepts the same alg as the token claims, it verifies with the public key as the HMAC secret → valid signature.

Defense: pin the expected algorithm in verification. Never trust the alg from the token header.

Weak HMAC secret — short or guessable secret used for HS256 JWTs. Attacker brute-forces, then signs arbitrary tokens.

Defense: use long (32+ random bytes) secrets, store in HSM or secrets manager. Rotate periodically.

Missing expiration — JWTs are stateless. Once issued, can't be revoked without other mechanisms. Long-lived JWTs (>1 hour) without revocation = unhappy.

Defense: short expiration (15 minutes is common), refresh tokens for renewing, server-side blocklist for emergency revocation.

Sensitive data in payload — JWTs are base64-encoded, not encrypted. Anyone with the token can read the payload.

Defense: never put passwords, full PII, or anything beyond identifiers in payloads. Use the JWT as a key into your database for the real data.

A safer JWT pattern in Django

from rest_framework_simplejwt.settings import api_settings

# In settings.py
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': True,
    'ALGORITHM': 'HS256',  # Pin algorithm
    'SIGNING_KEY': settings.SECRET_KEY,  # Long random secret
    'AUDIENCE': 'djangozen-api',
    'ISSUER': 'djangozen',
    'JTI_CLAIM': 'jti',  # For revocation by ID
}

BLACKLIST_AFTER_ROTATION means once you refresh, the old refresh token can't be reused — defeats refresh-token theft.

OAuth 2.0 — when delegation goes wrong

OAuth lets users grant apps access to their accounts on other services without sharing passwords. It's standard for "Sign in with Google" patterns. It's also a vulnerability rich text format.

Implicit flow — don't use it

The old response_type=token flow returns access tokens directly in URLs. They leak to referrers, browser history, server logs.

Modern apps use authorization code flow with PKCE — the secure default.

Authorization code injection

Attacker tricks user into authorizing the legitimate app, captures the authorization code mid-flight, redeems it themselves.

Defense: PKCE (Proof Key for Code Exchange). The legitimate client generates a verifier; only it can exchange the code for tokens.

Redirect URI laxness

Apps that accept any redirect_uri value let attackers receive tokens at their own domain.

Defense: exact-match registered redirect URIs. No regex matching, no wildcard subdomains.

State parameter missing

The state parameter in OAuth is a CSRF defense — links a request to its response. Missing or unvalidated state allows CSRF in auth flows.

Defense: always include and validate the state parameter. Tie it to the user's session.

Consent screen phishing

Attacker registers an OAuth app with a name like "Your Company IT Support." Sends users a link to authorize. Users see "Authorize Your Company IT Support to access your account" and click yes. Attacker now has long-lived access token without ever knowing the password.

Defenses:

  • OAuth app approval workflows (admins approve new app integrations before users can authorize)
  • User education on OAuth consent screens
  • Periodic review of authorized OAuth apps
  • Revocation tooling

SSO — Single Sign-On bugs

SSO (SAML, OpenID Connect, OAuth-based) consolidates auth across services. It also consolidates failure modes.

SAML response replay

SAML responses can sometimes be replayed if not properly time-bound or one-use.

Defense: validate timestamps strictly (typically a 5-minute window), track response IDs to detect replays.

SAML XML signature wrapping

Attackers manipulate the XML structure so a malicious payload is processed while the signature still verifies against the original (signed) content.

Defense: validate that the signed XML element is the one being processed. This is a parser-level concern; use mature SAML libraries (python-saml2, pysaml2) and keep them updated.

Account linking attacks

User has an account on your service with email user@example.com. Attacker creates a Google account user@example.com (or controls a corporate Google Workspace). Attacker clicks "Sign in with Google" on your service. Your service auto-links the Google identity to the existing local account.

Defense: never auto-link based on email alone. Require explicit verification (e.g., send email to the existing account asking to confirm linking).

Password reset — the bypass everyone forgets

The password reset flow is often the easiest auth bypass to find because:

  1. It's specifically designed to grant access without the current password
  2. It's exercised much less than login
  3. Edge cases accumulate

Token leakage in referer

Reset URL: https://yourapp.com/reset/?token=abc123. User clicks. Browser sends Referer: https://yourapp.com/reset/?token=abc123 to any external resource on the post-reset page. Token leaks.

Defense: strip query parameters after using the token. Redirect to a clean URL. Use Referrer-Policy: strict-origin header.

Token reuse

Tokens that don't get invalidated after use can be replayed.

Defense: one-use tokens. Invalidate immediately on consumption.

Token guessability

UUIDs are fine. Sequential IDs, short tokens, timestamps — not fine.

Account enumeration via reset

"If we have an account with that email, we sent a reset link." Good. "No account found with that email." → enumeration. Different response times → enumeration even with same message.

Defense: identical responses regardless of whether the email exists; identical timing (use a constant delay or async background processing).

Email spoofing in reset

Reset link generated with Host: attacker.com header → email goes to user, link is to attacker.

Defense: use canonical URLs from settings, not request-derived hosts.

Account takeover (ATO) defenses — putting it together

Layered controls that change ATO economics:

Login

  • Strong password rules (no max length, blocked breached passwords)
  • Rate limiting per IP and per account
  • MFA required (phishing-resistant for staff)
  • Anomaly detection (impossible travel, new device)
  • Step-up auth on sensitive actions

Session

  • Short session lifetime
  • Secure, HttpOnly, SameSite cookies
  • Device binding where possible
  • All sessions listable + revocable
  • Logout invalidates everywhere

Password reset

  • One-use tokens
  • Short expiry (10-15 minutes)
  • Email-based delivery only (never SMS)
  • Identical responses on enumeration probes
  • Step-up after reset (require additional verification before allowing email/MFA changes)

Recovery

  • Recovery methods enrolled before being needed (not after lockout)
  • Recovery via multiple factors (e.g., email + phone + backup codes)
  • Manual review for high-value accounts
  • Cool-down periods after recovery (limit actions for N hours after self-service recovery)

Detection

  • Logging of every auth event (success, failure, MFA, password change, email change)
  • Alerts on patterns: many failures, MFA bypass attempts, password change followed by email change
  • User-facing notifications: "Your password was changed"
  • Audit logging that survives account compromise

Closing thought

Authentication is the entire access control story compressed into a few minutes per user. Most web app breaches at the technical level are auth breaches at some layer — credentials, sessions, tokens, OAuth, SSO. Investing here pays off everywhere.

Tutorial 9 covers what happens when an attacker has authenticated as someone — the lateral movement, the persistence, the action-on-objective.