How authentication actually breaks in modern web apps — session theft, JWT confusion, OAuth flaws, SSO race conditions, and the controls that actually work.
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.
Even mature frameworks have edge cases.
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
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.
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.
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:
re-enter password before changing emailUsers 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()
JSON Web Tokens are everywhere in modern auth. They're also one of the most consistently misimplemented protocols.
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).
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.
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 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.
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.
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.
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.
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.
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:
SSO (SAML, OpenID Connect, OAuth-based) consolidates auth across services. It also consolidates failure modes.
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.
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.
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).
The password reset flow is often the easiest auth bypass to find because:
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.
Tokens that don't get invalidated after use can be replayed.
Defense: one-use tokens. Invalidate immediately on consumption.
UUIDs are fine. Sequential IDs, short tokens, timestamps — not fine.
"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).
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.
Layered controls that change ATO economics:
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.