Authentication Deep Dive
Breadth layernotes-security-core-knowledge.md
Password Security
Last verified2026-06
The one idea: a password hash must be SLOW on purpose
You never store the password itself — you store a hash, a one-way fingerprint of it. To log you in, the server hashes what you typed and checks it equals the stored hash. If the database leaks, the attacker gets only fingerprints and has to guess: pick a candidate password, hash it, see if it matches.
So the entire game is one number: how many guesses per second can the attacker make? Everything about password hashing exists to drive that number down.
A normal hash like SHA-256 is built to be fast — perfect for checking file integrity, a disaster for passwords. On a modern GPU an attacker computes billions of SHA-256 hashes per second, so a leaked database of SHA-256 password hashes is cracked almost for free. The fix is a hash that is deliberately expensive to compute, tuned so one login costs the server a few milliseconds but costs the attacker that same few milliseconds on every one of their billions of guesses.
Hashing Algorithms — Why It Matters
| Algorithm | Appropriate? | Speed (hashes/sec on GPU) | Notes |
|---|---|---|---|
| MD5 | Never | ~10 billion | Broken; rainbow tables exist for everything |
| SHA-256 | Never for passwords | ~8 billion | Too fast; GPU cracks billions/sec |
| bcrypt (cost=12) | Yes | ~10,000 | Adaptive; increase cost as hardware improves |
| scrypt (N=32768) | Yes | ~1,000 | Memory-hard; defeats GPU attacks |
| Argon2id | Best choice | ~500 | Memory + CPU hard; PHC winner |
Argon2id configuration
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=2, # iterations
memory_cost=65536, # 64 MB RAM required per hash
parallelism=2, # threads
)
hash = ph.hash("user_password")
ph.verify(hash, "user_password") # raises exception on mismatchWhy Argon2id is so hard to break
You can make a single guess expensive in two ways: cost the attacker CPU time, or cost them memory. Argon2id does both, and the memory part is what really breaks the attacker's economics.
The problem with "slow but small" (bcrypt). bcrypt is CPU-hard — each guess takes real time — but it uses only about 4 KB of memory. Attackers don't crack on normal CPUs; they use GPUs (thousands of small cores) and custom chips (FPGAs, ASICs) that pack in even more cores. When each guess needs only 4 KB, you can run tens of thousands of guesses in parallel on one card, because tiny memory is cheap to replicate next to each core. bcrypt is still acceptable, but it doesn't fight this parallelism.
What "memory-hard" means (Argon2id)Argon2id forces each guess to allocate a big block of RAM — say 64 MB — and to read and write all over it in a sequence you can't shortcut. Now the attacker's hardware advantage collapses:
- A GPU has thousands of cores but limited memory and memory bandwidth. If each guess needs 64 MB, a 16 GB card can run only ~250 guesses at once instead of tens of thousands. The cores sit idle waiting on memory.
- ASICs lose their edge too. Their trick is stamping out millions of cheap compute units, but RAM takes physical silicon area that costs the attacker the same as it costs anyone. Memory is the great equaliser: a megabyte costs the attacker what it costs the defender, unlike raw compute.
Why you can't cheat by using less memory. The obvious dodge is "store less, recompute the missing values on the fly" — trade memory back for compute. Argon2 is designed so that dropping below the target memory makes the required compute blow up far faster than the memory you saved, so it's never worth it. It also uses data-dependent addressing in its later passes: which memory cell it touches next depends on the data it just computed, so the attacker can't predict and pre-fetch the access pattern or pipeline it on a GPU.
Why the "id" in Argon2idArgon2 has three variants:
data-dependent memory access. Maximum cracking resistance, but the timing of those accesses can leak information through side channels (a concern if untrusted code shares the machine).
data-independent access. Side-channel safe, but slightly weaker against GPU cracking.
a hybrid: the first pass is data-independent (side-channel safe), later passes are data-dependent (cracking-resistant). Best of both, and the variant recommended by RFC 9106 (2021).
The three knobsmemory_cost (RAM per guess — the main weapon), time_cost (number of passes over that memory), and parallelism (independent lanes). You turn these up as hardware improves; the defender pays the cost once per login, the attacker pays it on every guess. Argon2id won the Password Hashing Competition in 2015.
Memory hookmake the attacker rent RAM, not just burn CPU. A fast hash (SHA-256) is a sprint the GPU wins easily. bcrypt makes it a long run — better, but the GPU just fields thousands of runners. Argon2id makes every runner drag a 64 MB suitcase: now you can only fit a few hundred on the track at once, and no custom chip makes RAM cheaper than it is for the defender. CPU-hard slows one guess; memory-hard caps how many guesses run in parallel — and parallelism is the attacker's whole advantage.
Salting
A salt is a unique random value generated per password and stored alongside the hash. Purpose: prevents precomputed rainbow tables from working, and ensures identical passwords hash differently.
hash = Argon2id(password + salt)
stored = salt + "$" + hashModern password hashing libraries (bcrypt, Argon2) handle salting automatically — never implement this manually.
Credential Stuffing vs Brute Force
try all combinations against a specific account. Mitigated by rate limiting, CAPTCHA, lockout.
use leaked username:password pairs from one breach against another service. Mitigated by MFA, breached-password detection (HIBP API), device fingerprinting.
try one common password against many accounts (avoids per-account lockout). E.g., Password1! against all 50,000 AD accounts.
OAuth 2.0 — Flows and Security
OAuth 2.0 is a delegation protocol — it lets a user grant a third-party application access to their resources on another service, without sharing their password.
Memory hookOAuth is a valet keyA valet key starts the car and opens the door but won't open the trunk or glovebox — it's scoped, delegated access you hand a stranger without giving them your master key (password). That's OAuth: the app gets a token that can do some things on your behalf, and you can revoke it without changing your password. The crucial interview line: OAuth is for authorization (what you can access), NOT authentication (who you are) — using a raw OAuth access token to "log someone in" is the classic mistake that OIDC was created to fix.
Roles
| Role | Who they are |
|---|---|
| Resource Owner | The user who owns the data |
| Client | The third-party application wanting access |
| Authorisation Server | Issues tokens (e.g. Okta, Auth0, Google) |
| Resource Server | API that holds the user's data |
The flow in plain English
Before the raw HTTP below, here's what actually happens when you click "Log in with Google" on some app:
-
You click "Log in with Google" on the app (the Client)Browser
-
The app redirects you to Google (the Authorisation Server)Browser
The request names which app is asking (
client_id), what it wants (scope), where to come back (redirect_uri), and a randomstateagainst CSRF. -
Google shows login + consent; you authenticate and approveGoogle
Your password and MFA only ever go to Google — the app never sees them.
-
Google redirects you back with a short-lived authorization codeBrowserfront channel
Not the token yet. The code is useless on its own and expires in seconds to minutes.
-
The app's server swaps the code for an access tokenApp serverback channel
A direct server-to-server call to Google (with PKCE's
code_verifier). Now the app can call the API on your behalf.
Why the two-step dance (a code first, then the token)? So the powerful access token never travels through the browser or a URL, where it could be logged by a proxy, sit in history, or leak through a referrer header. Only the throwaway code rides through the browser — and that code can only be redeemed by the app that started the flow, which is exactly what PKCE enforces.
Authorisation Code Flow + PKCE (The Correct Flow)
User → Client: "Login with Google"
Client → Auth Server: GET /authorize?
response_type=code
&client_id=...
&redirect_uri=https://app.com/callback
&scope=openid profile email
&state=<random> ← CSRF protection
&code_challenge=<sha256(verifier)> ← PKCE
&code_challenge_method=S256
Auth Server → User: Login page
User → Auth Server: Authenticate + Consent
Auth Server → Client: GET /callback?code=<auth_code>&state=<random>
Client: verify state matches
Client → Auth Server: POST /token
grant_type=authorization_code
&code=<auth_code>
&redirect_uri=https://app.com/callback
&code_verifier=<original_verifier> ← PKCE verification
&client_id=...
[&client_secret=... if confidential client]
Auth Server → Client: {
access_token: "...",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "...",
id_token: "..." ← OIDC only
}PKCE (Proof Key for Code Exchange)
- Client generates a random
code_verifier(43–128 chars) - Sends
SHA256(code_verifier)ascode_challengein the auth request - Sends the raw
code_verifierat token exchange - Prevents authorisation code interception — stolen code is useless without the verifier
- Mandatory for public clients (SPAs, mobile apps); strongly recommended for all
Memory hookPKCE is a coat-check ticketYou keep a secret stub (
code_verifier) and hand over only its hash (code_challenge) when you start. At the end you present the original stub to prove you're the same party who began the flow. A thief who intercepts the authorization code can't redeem it — they never had the stub. This matters most for mobile/SPA apps that can't keep a client secret, because the app binary is on the attacker's device. Mnemonic: PKCE = "Pixie" dust that binds the start of the flow to its finish.
State parameterrandom nonce tied to the user's session; verified on callback. Prevents CSRF on the OAuth flow (attacker starts their own auth flow and tricks victim into completing it with their session).
Common OAuth Vulnerabilities
| Vulnerability | Description | Mitigation |
|---|---|---|
Missing state | CSRF on auth flow | Always validate state |
Open redirect in redirect_uri | Steal auth code by registering redirect_uri that redirects | Strict redirect URI allowlist; exact match |
| Token stored in localStorage | XSS can steal it | Store access token in memory; refresh token in HttpOnly cookie |
| Implicit flow (deprecated) | Token in URL fragment → logged by servers/proxies | Use Auth Code + PKCE instead |
| Missing PKCE | Auth code interceptable by malicious app | Require PKCE for all public clients |
| Token scope too broad | Overprivileged access | Request minimal scopes; use separate tokens per resource |
OpenID Connect (OIDC)
What is this?
OAuth gives an app permission to do things on your behalf, but it deliberately says nothing about who you are. OpenID Connect (OIDC) is a thin layer on top of OAuth 2.0 that adds exactly that missing piece: a standard, verifiable answer to "who just logged in?"
It reuses the entire OAuth Authorization Code + PKCE flow — same redirects, same endpoints — and changes two things: you add the openid scope to the request, and you get back one extra item alongside the access token: an ID Token.
Access token vs ID token — the distinction that matters
This is the single most-tested OIDC idea:
a capability. "Whoever holds this may call these APIs." It's meant for the resource server (the API), and you're not supposed to read its contents. Possessing one does not prove identity — it could have been minted for any app and replayed.
a statement of identity. "The issuer confirms this user authenticated, for your app, at this time." It's a signed JWT meant for the client to read and verify.
The classic bug OIDC was invented to fix: developers used a raw OAuth access token to "log the user in." But an access token is just a bearer key — an attacker who obtains a token issued for another app can present it to yours and impersonate the user. The ID token closes this because it carries an aud (audience) claim naming your client, and a nonce tying it to your specific login request.
// Decoded ID Token payload
{
"iss": "https://accounts.google.com", // who issued it
"sub": "1234567890", // stable unique user ID
"aud": "your-client-id", // who it was issued FOR (must be you)
"exp": 1700000000, // expiry
"iat": 1699996400, // issued-at
"email": "user@example.com",
"email_verified": true,
"nonce": "random-nonce" // ties token to YOUR auth request
}Validations required when receiving an ID Token (each line is a real attack you're blocking):
- Verify the signature using the issuer's public key — otherwise the token is forgeable.
- Check
issmatches the expected issuer — otherwise a different IdP's token is accepted. - Check
audmatches yourclient_id— this is what stops a token minted for another app being replayed at yours. - Check
expis in the future — otherwise expired tokens are replayed. - Check
noncematches what you sent in the auth request — ties the token to this login, blocks replay of an old one. - Check
iatis not implausibly old — defends against stale-token replay.
Where the keys come from
The issuer publishes a discovery document at https://issuer/.well-known/openid-configuration, which points to a jwks_uri (JSON Web Key Set) holding its public signing keys. Your app picks the key matching the kid in the token header and verifies the signature. Nothing secret is shared — verification uses only public keys.
Memory hookOAuth authorizes, OIDC authenticatesWhen someone "logs in with Google," the part that proves who they are is OIDC's ID token (signed, audience-bound, nonce-bound), not the OAuth access token (a bearer capability that proves nothing about identity). Mix them up and you get the single most common federation bug: treating a stealable access token as proof of login.
SAML 2.0 — Enterprise SSO
What is this?
SAML does the same job as OIDC — let you log into one app using your identity from a central provider — but it predates the OAuth/OIDC web-API era. Instead of JSON tokens it uses signed XML documents, and it's everywhere in corporate single sign-on (Okta, Azure AD / Entra, PingFederate, Google Workspace).
Two parties:
the system that knows who you are and checks your password/MFA (e.g. Okta).
the app you're trying to use (e.g. Salesforce).
The trust is cryptographic and one-way: the IdP writes a signed statement — "this is alice@corp.com, role admin" — called an assertion, and signs it with its private key. The SP holds the IdP's public certificate and checks the signature. If valid, the SP trusts the contents and logs you in. No password ever reaches the SP.
Quick contrast: SAML is XML carried by a browser form POST, dominant in workforce/B2B SSO; OIDC is JSON/JWT over REST, dominant in consumer, mobile, and single-page apps. Same goal (federated SSO), different era and shape.
Flow (SP-initiated)
User → Service Provider (SP): access protected resource
SP → User: redirect to IdP with SAMLRequest (base64 XML)
User → IdP: authenticate
IdP → User → SP: HTTP POST /acs with SAMLResponse (signed XML assertion)
SP: verify signature → grant accessSAML Assertion Structure
<saml:Assertion>
<saml:Issuer>https://idp.example.com</saml:Issuer>
<ds:Signature>...</ds:Signature> ← covers what exactly?
<saml:Subject>
<saml:NameID>user@example.com</saml:NameID>
</saml:Subject>
<saml:AttributeStatement>
<saml:Attribute Name="role">
<saml:AttributeValue>admin</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>SAML Attacks
XML Signature Wrapping (XSW)The signature covers a specific element by ID. The attack moves the signed element and inserts a malicious one, tricking parsers that evaluate the assertion differently from the signature verifier.
<!-- Original signed assertion (ID="legit") -->
<Assertion ID="legit"><Signature>...</Signature><NameID>user</NameID></Assertion>
<!-- XSW: wrap it, add malicious assertion -->
<Assertion ID="evil"><NameID>admin</NameID>
<Assertion ID="legit"><Signature>...</Signature><NameID>user</NameID></Assertion>
</Assertion>
<!-- Signature validates "legit", but SP uses "evil" (first found) -->MitigationsUse a hardened SAML library; validate signature covers the assertion element you're using; check InResponseTo; enforce replay protection via AssertionID cache.
JWT — Structure, Attacks, Mitigations
What is this?
A JWT (JSON Web Token, said "jot") is a small, self-contained token that carries claims — facts like "user = 1234, role = admin, expires at 17:00" — in a format anyone can read but only the issuer can produce. Its whole value is being stateless: the server can verify a JWT with just a key, no database lookup, because the token carries its own proof of authenticity in the signature.
That self-contained nature is also its danger. Everything rests on verifying the signature correctly, so every attack below is ultimately a way to make the server skip or weaken that check. And note: a JWT is signed, not encrypted — the payload is only base64-encoded, readable by anyone who holds the token, so never put secrets in it.
Structure
header.payload.signature
Header: base64url({ "alg": "RS256", "typ": "JWT" })
Payload: base64url({ "sub": "1234", "role": "user", "exp": 1700000000 })
Signature: RS256(base64url(header) + "." + base64url(payload), private_key)Attack 1: alg: none
Attacker sets "alg": "none" and removes the signature. Vulnerable libraries that accept any algorithm will trust the unsigned token.
# Malicious token
import base64, json
header = base64.urlsafe_b64encode(json.dumps({"alg":"none","typ":"JWT"}).encode()).rstrip(b'=')
payload = base64.urlsafe_b64encode(json.dumps({"sub":"admin","role":"admin"}).encode()).rstrip(b'=')
token = header + b'.' + payload + b'.' # empty signatureMitigationPin the algorithm server-side — never accept what the token header says. jwt.decode(token, key, algorithms=["RS256"]), not algorithms=["RS256", "none"].
Memory hooknever let the envelope tell you how to check the seal. Both big JWT attacks (
alg:noneand RS256→HS256 confusion) share one root cause: the server trusts thealgfield in the attacker-controlled header to decide how to verify.alg:nonesays "don't check the seal at all"; algorithm confusion says "check this asymmetric signature as if it were a symmetric one, using the public key as the password." The single defense for both: hard-code the expected algorithm server-side and ignore the header's claim. Remember: the header is the attacker's; the verification rules are yours.
Attack 2: Algorithm Confusion (RS256 → HS256)
If the server accepts both RS256 (asymmetric) and HS256 (symmetric):
- Get the server's RSA public key (often at
/.well-known/jwks.jsonor/oauth/public-keys) - Create a new token with
"alg": "HS256"and sign it with the public key as the HMAC secret - The server's HS256 code uses the public key to verify — it passes
MitigationNever allow the algorithm to be specified in the token. Dedicate a key per algorithm; don't share keys between RS256 and HS256 contexts.
Attack 3: Weak Secret (HS256 Brute Force)
HS256 tokens signed with weak secrets can be cracked offline:
hashcat -a 0 -m 16500 token.txt wordlist.txt
# or
python3 jwt_tool.py <token> -C -d wordlist.txtMitigationUse 256-bit random secrets for HS256; prefer RS256/ES256 (asymmetric) for distributed systems where multiple services verify tokens.
Attack 4: JWT Claim Tampering Without Signature Check
If the application doesn't verify the signature — or uses the wrong key — any payload can be injected.
Secure JWT Usage
import jwt
from datetime import datetime, timedelta
SECRET = os.environ["JWT_SECRET"] # 32+ random bytes
def create_token(user_id: str, role: str) -> str:
return jwt.encode({
"sub": user_id,
"role": role,
"iat": datetime.utcnow(),
"exp": datetime.utcnow() + timedelta(hours=1),
"jti": secrets.token_hex(16), # unique ID — enables revocation
}, SECRET, algorithm="HS256")
def verify_token(token: str) -> dict:
# Pinning algorithm — never accept what the token claims
return jwt.decode(token, SECRET, algorithms=["HS256"], options={"require": ["exp", "sub"]})WebAuthn / FIDO2 / Passkeys
Why It's Better Than Everything Else
| Property | Password | TOTP | WebAuthn |
|---|---|---|---|
| Phishing resistant | No | No | Yes (origin-bound) |
| Server breach risks credentials | Yes (hash) | Yes (seed) | No (public key only) |
| User needs to type something | Yes | Yes | No |
| Requires extra hardware | No | Often | No (passkeys use device biometrics) |
How It Works
Registration
- Server generates a
challenge(random bytes) - Browser calls
navigator.credentials.create()with the challenge - Authenticator (YubiKey / platform authenticator) creates a new key pair; private key never leaves the device
- Authenticator signs the challenge + client data + origin using the private key
- Public key + credential ID sent to server → stored in the database
Authentication
- Server generates a new
challenge - Browser calls
navigator.credentials.get()with the challenge - Authenticator signs the challenge using the private key for the matching credential
- Server verifies the signature using the stored public key
- Origin is cryptographically bound — a phished login on
bank-secure.evil.comwon't produce a valid signature forbank.com
Passkeys
Passkeys are WebAuthn credentials synced across devices (iCloud Keychain, Google Password Manager). The private key is exported in encrypted form and synced to other devices. Provides phishing resistance with the convenience of passwords.
Kerberos — AD Authentication
Protocol Flow
1. AS-REQ: Client sends username + timestamp encrypted with password hash
→ KDC (Key Distribution Centre, DC)
2. AS-REP: KDC returns:
- TGT (Ticket Granting Ticket): encrypted with krbtgt hash
- Session key: encrypted with client's key
3. TGS-REQ: Client presents TGT to get service ticket
- Sends: TGT + authenticator (encrypted with session key)
4. TGS-REP: KDC returns TGS (service ticket) encrypted with target service's key
5. AP-REQ: Client presents TGS to target service → authenticatedMemory hookKerberos is a theme parkAt the gate you show ID once and get a wristband (TGT) from the ticket office (KDC) — now you don't show ID again all day. To ride a specific attraction you swap the wristband for a ride ticket (TGS) for that ride, and the ride operator (the service) just checks the ticket. The names map cleanly: AS-REQ/REP = get the wristband, TGS-REQ/REP = get a ride ticket, AP-REQ = hand the ticket to the ride. Why it matters for attacks: a Golden Ticket forges the wristband (krbtgt key = the whole park's master), a Silver Ticket forges one ride ticket (one service's key, never touches the KDC so it's stealthier). Mnemonic: Gold = the master gate, Silver = a single door.
Memory hookwhy Kerberoasting works at allAny authenticated user can request a service ticket for any service, and that ticket is encrypted with the service account's password hash. So the attacker asks for the ticket legitimately, then cracks it offline at their leisure — no failed logins, no lockouts, nothing noisy on the wire. That's why service accounts with weak, human-set passwords (and high privileges) are the prize. The fix is long random passwords (or gMSAs, which use 120-char machine-managed passwords that are uncrackable).
Attack Techniques
KerberoastingAny domain user can request a TGS for any SPN (Service Principal Name). The TGS is encrypted with the service account's NTLM hash — take it offline and crack.
# Enumerate SPNs and request tickets
python3 GetUserSPNs.py domain/user:password -dc-ip 10.10.10.10 -request
# Crack with hashcat
hashcat -m 13100 kerberoast_hash.txt /usr/share/wordlists/rockyou.txtAS-REP RoastingAccounts with "Do not require Kerberos preauthentication" set can be attacked without credentials.
python3 GetNPUsers.py domain/ -usersfile userlist.txt -dc-ip 10.10.10.10
hashcat -m 18200 asrep_hash.txt wordlist.txtGolden TicketForge a TGT using the krbtgt NTLM hash (obtained via DCSync or DC compromise). Valid for 10 years by default. Gives persistent domain admin access even after password resets (except krbtgt reset).
# With mimikatz (post-DC-compromise)
lsadump::dcsync /user:krbtgt
kerberos::golden /user:Administrator /domain:corp.local /sid:S-1-5-21-... /krbtgt:<hash> /id:500Silver TicketForge a TGS using the target service account's NTLM hash. Doesn't touch the KDC → harder to detect. Scoped to one service.
Pass-the-TicketSteal a TGT/TGS from LSASS memory and inject into current session.
# With mimikatz
sekurlsa::tickets /export
kerberos::ptt ticket.kirbiMFA Methods Compared
| Method | How it works | Phishing resistant? | SIM swap risk | Offline capable |
|---|---|---|---|---|
| SMS OTP | Code sent via SMS | No | Yes | No |
| TOTP (app) | HMAC(secret, time/30) | No (real-time phishing proxies work) | No | Yes |
| TOTP (hardware, e.g. RSA SecurID) | Same but on dedicated device | No | No | Yes |
| FIDO2 / WebAuthn | Origin-bound key pair | Yes | No | Yes |
| Push notification (Duo, Okta) | Approve/deny on app | No (fatigue attacks) | No | No |
| Backup codes | Static one-time codes | No | No | Yes |
MFA fatigue attackbombard the user with push notifications until they approve one by mistake. Mitigated by: number matching (user must enter the code shown in the push app), geographic/time anomaly detection, rate limiting push requests.
Session Management
Secure Session Architecture
User authenticates →
Server generates: session_id = secrets.token_urlsafe(32) # 256 bits
Stores: session_store[session_id] = {user_id, created_at, last_active, ip, ua}
Sets cookie: Set-Cookie: session=<session_id>; HttpOnly; Secure; SameSite=Strict; Path=/
Subsequent requests →
Server looks up session_id in session_store
Validates: not expired, not revoked, optional IP/UA binding
Returns appropriate responseSession Cookie Attributes
Set-Cookie: session=abc123;
HttpOnly; # JS cannot read — blocks XSS-based theft
Secure; # HTTPS only
SameSite=Strict; # CSRF protection
Path=/; # scope to root
Max-Age=3600; # 1 hour expiry
Domain=app.example.com # don't set Domain if you want to exclude subdomainsSession Attacks
steal a session cookie (via XSS, network sniffing, MITM) and replay it
attacker sets the victim's session ID before login; attacker then reuses it post-login
make cross-site requests that include the victim's session cookie
Interview Questions
The client redirects the user to the authorization server with the requested scopes, a random state, and a PKCE code_challenge (the hash of a secret verifier). The user authenticates and consents, and the auth server redirects back with a short-lived authorization code. The client then exchanges that code at the token endpoint, sending the original code_verifier — the server hashes it and checks it matches the earlier challenge before issuing tokens. state is a random value tied to the user's session and checked on the callback; it prevents CSRF on the flow, where an attacker tricks the victim into completing an auth flow the attacker started, linking the victim's session to the attacker's account. PKCE separately prevents a stolen authorization code from being redeemed, which matters for mobile/SPA clients that can't keep a secret.
alg:none JWT vulnerability and how do you fix it?alg:none is a legacy JWT option meaning "unsigned." If a library honors the token's own alg header, an attacker can set alg to none, strip the signature, and forge any claims — like making themselves admin — and a naive verifier accepts it. The fix is to never trust the header's algorithm: pin the expected algorithm server-side, e.g. decode with algorithms=["RS256"] only. The same root cause drives the RS256→HS256 confusion attack, so the general rule is the verification algorithm is the server's decision, not the token's.
Authentication is the one-time act of proving who you are — password, MFA, WebAuthn. Session management is how the server remembers that you're authenticated across subsequent requests, usually via a session cookie or token. Authentication failures include weak hashing, credential stuffing, phishable MFA, and missing rate limiting. Session failures include hijacking a stolen cookie, session fixation (the attacker plants a session ID before login), insufficient entropy in session IDs, missing HttpOnly/Secure/SameSite flags, and not rotating or revoking sessions on privilege change or logout. A strong auth step is undermined if the session that follows it can be stolen or replayed.
WebAuthn binds each credential to the origin it was registered for, and the browser includes the actual origin in the data the authenticator signs. When you register at bank.com, the authenticator creates a key pair scoped to bank.com; the private key never leaves the device. At login, the signature covers a server challenge plus the real origin. A phishing page on evil.com can relay traffic, but the authenticator will sign for origin evil.com, not bank.com, so the signature won't validate against bank.com's stored public key — and the authenticator simply has no credential for evil.com. There's also no shared secret to steal: the server only ever stores a public key. That origin-binding is what makes it fundamentally phishing-resistant where passwords and OTPs are not.
Any authenticated domain user can request a service ticket (TGS) for any service principal name, and that ticket is encrypted with the service account's password hash. The attacker requests tickets for service accounts, extracts them, and cracks them offline — no failed logons, no lockouts, nothing noisy. An account is vulnerable when it has an SPN registered (so it's targetable), a weak human-chosen password (so it's crackable offline), and ideally high privileges (so the payoff is large). The mitigation is long random passwords or group-managed service accounts, which use 120-character machine-rotated passwords that are effectively uncrackable, plus monitoring for anomalous TGS request volume.
SMS OTP is the weakest — vulnerable to SIM swapping, SS7 interception, and real-time phishing — but it's better than nothing and fine as a low-assurance fallback for consumer accounts. TOTP (an authenticator app) removes the SIM-swap risk and works offline, but it's still phishable: a real-time proxy can relay the code, and the seed is at risk if the server is breached. FIDO2/WebAuthn is the strongest — origin-bound, phishing-resistant, nothing crackable stored server-side — and is what I'd recommend for anything privileged or high-value, ideally as the primary factor. So: FIDO2 wherever feasible, TOTP as a solid second choice, SMS only as a last-resort fallback.
A Golden Ticket is a forged Kerberos TGT created using the krbtgt account's hash, which an attacker obtains after compromising a domain controller (e.g., via DCSync). Because they hold the key that signs all tickets, they can mint a TGT for any user with any privileges, valid by default for years — persistent domain dominance that survives user password resets. Detection is hard but possible: TGTs with anomalous lifetimes, tickets for users that never did an AS-REQ, or RC4-encrypted tickets in an AES environment. Remediation requires resetting the krbtgt password twice (to flush both the current and previous key), which invalidates all existing tickets — and really, full DC rebuild and incident response, since a Golden Ticket means the domain was fully compromised.
SHA-256 is disqualified outright — it's a fast general-purpose hash, billions per second on a GPU, so a leaked database is trivially cracked. Both bcrypt and Argon2id are deliberately slow. bcrypt is CPU-hard and still acceptable, but it uses a fixed small amount of memory, so attackers can parallelize it cheaply on GPUs/ASICs. Argon2id is memory-hard — it forces each guess to consume significant RAM, which makes massive parallel cracking expensive — and it's the Password Hashing Competition winner, combining resistance to both GPU attacks and side channels. So Argon2id is the current best choice; bcrypt is the acceptable legacy option. Always with a per-password salt.
OAuth 2.0 is an authorization framework — it issues access tokens that let an app act on your behalf with scoped permissions, but it says nothing standard about who you are. OIDC is a thin identity layer on top of OAuth that adds the ID Token, a signed JWT with verified claims about the user (subject, issuer, audience, email), plus a standard userinfo endpoint and discovery. The common mistake OIDC fixes: people used OAuth access tokens to "log in" users, but an access token is just a bearer capability — possessing it doesn't prove identity, and tokens can be replayed across apps. OIDC's ID Token, with its audience and nonce, is the proper authentication mechanism. Mnemonic: OAuth authorizes, OIDC authenticates.
In XML Signature Wrapping, the attacker exploits the gap between what the signature verifier checks and what the application actually reads. A SAML signature covers a specific element by ID. The attacker keeps the original signed assertion intact (so the signature still validates) but wraps it inside a new structure and injects a forged assertion — say, changing the user to admin. If the SP's signature check finds the legitimate signed element while its business logic reads the first/forged assertion, it grants access based on attacker-controlled data with a "valid" signature. Mitigations: use a hardened SAML library that verifies the signature covers exactly the element being consumed, validate the full document schema, check InResponseTo, and enforce assertion-ID replay protection.