Authentication, Sessions & Access Control
Software Security · Nutthakorn Chalaemwongwan
Today
- Authn vs authz
- Session management & JWT pitfalls
- OAuth2 / OIDC at a glance
- IDOR & broken access control
- 🎮 Game: IDOR Treasure Hunt + JWT Forgery
Recap — Week 5
- XSS steals sessions → today we manage them
- Client-side trust is limited — enforce on the server
Authn vs Authz
- Authentication — who are you? (login, MFA)
- Authorization — what are you allowed to do?
- Most breaches today are authz failures
- Maps to A01 Broken Access Control, A07 Authentication Failures
Sessions
- Server issues a session token after login
- Classic pattern: stored in a cookie (
HttpOnly,Secure,SameSite) - Risks: fixation, predictable IDs, no expiry, no logout invalidation
- Today's lab is 100% JWT bearer-token — no cookies at all. Of these four risks, only "no expiry" is what you'll actually exercise (the fix adds a 15-min
expclaim); fixation/predictable-ID/logout-invalidation are cookie-session concepts, general knowledge, not tested here
JWT pitfalls
alg: noneaccepted → forge any token- Weak/guessable HMAC secret → re-sign
- Not checking
exp/aud/ signature at all - Sensitive data in payload (it's only base64!)
The fix: verify the signature — one check, every endpoint
# vulnerable: 'none' allowed + weak secret -> any forged token passes
if alg == "none": jwt.decode(token, options={"verify_signature": False})
SECRET = "secret"
# fixed: pin the algorithm, strong secret, require exp + aud
jwt.decode(token, SECRET, algorithms=["HS256"],
audience=AUD, options={"require": ["exp", "aud"]})
- base64url is encoding, not a seal — only the signature check stops forgery
- Pin
algorithms=["HS256"](never"none") + a strong random secret from env - Fix it in one place (
current_user) → every endpoint is covered at once
JWT forgery in one picture
OAuth2 / OIDC (high level)
- Delegated access via tokens — don't share passwords
- OIDC adds identity (ID token) on top of OAuth2
- Common bugs: open redirect, missing state, token leakage
IDOR & broken access control
GET /api/orders/1 → your order
GET /api/orders/2 → someone else's 😱
- Object reference with no ownership check
- Vertical (become admin) vs horizontal (other users)
The fix: check ownership — a valid token is not permission
# vulnerable: authenticated, but the result is thrown away -> no Gate 2
def get_order(oid):
current_user() # WHO you are...
return jsonify(ORDERS[oid]) # ...but never 'may you see THIS?'
# fixed: deny-by-default ownership check on every object access
if order["owner"] != user: return 403
- Authentication answers who; authorization answers may you touch THIS object
- alice's real, valid token must still be refused bob's order
- Check ownership at every object access — deny by default
One request, two gates
Access control models
- MAC — Mandatory: system-enforced labels (military)
- DAC — Discretionary: owner grants access (file perms)
- RBAC — Role-Based: permissions via roles
- RuBAC — Rule-Based: conditions (time of day, IP)
Best practices: separation of duties · least privilege · implicit deny
Exercise — RBAC
| Role | Permissions |
|---|---|
| Developer | Read/write Git, JIRA, run unit tests |
| QA Engineer | Read Git, write test reports, JIRA, deploy to staging |
| Project Manager | Read Git, JIRA, view dashboard |
| Intern | Read Git, run unit tests |
- Who may deploy to staging?
- An intern must create bug entries — which role's perms to add?
- List all roles that can modify the codebase.
Reading the attack in the logs
Reconstruct the kill chain from Apache logs:
GET /login.php?username=' or 1=1 limit 1; -- a&... 302 ← SQLi admin
POST /upload.php 200 ← backdoor
GET /uploads/backdoor.php?cmd=ls%20-l 200 ← RCE
%20= space (URL-decode!) · logs let you trace what happened
CWE mapping
- CWE-639 — IDOR / missing ownership check
- CWE-347 — improper signature verification (
alg:noneforgery) - CWE-321 — use of a hardcoded/weak cryptographic key (the guessable HMAC secret)
Defenses
- Server-side authorization on every request (deny by default)
- Check ownership, not just authentication
- Strong session tokens + proper expiry/rotation
- Verify JWT signature,
alg,exp,aud; keep secrets strong - RBAC / ABAC enforced centrally
- Centralize the check — don't scatter
if role==...everywhere
Tool — Burp Suite workflow
| Tool | Use |
|---|---|
| Proxy | intercept & modify requests |
| Repeater | replay/tweak one request |
| Intruder | automate brute-force/fuzz |
| Decoder/Comparer | encode payloads / diff responses |
- Set scope, intercept, change a value (e.g.
price=1), forward - Test auth, sessions, IDOR, privilege escalation
🗺️ Game — IDOR Treasure Hunt + JWT Forgery
- Tamper object IDs to reach another user's data (horizontal — become "bob," not admin) → each secret = a flag
- Forge a weak JWT two ways:
alg:none, then crack the hardcoded HMAC secret - Defend: switch to
solution_app.py(already fixed), prove it blocks all three attacks (403 + two 401s), cite the exact fix line for each
Lab steps
📋 Worksheet 6 —
labs/week06-authn-authz/worksheet.md(Part 3) · kickoff:docker compose up→ http://localhost:8080
- Find IDOR endpoints; enumerate other users' objects
- Crack/forge a weak JWT (
alg:noneAND the weak HMAC secret — two separate graded attacks) - Switch to
solution_app.py; re-test: access denied - Cite the exact line that fixes each of the three attacks
Deliverable
- IDOR + JWT findings with impact
- Citation of the fix lines in
solution_app.pyfor all three attacks - Proof the forged token / cross-user access now fails
- + Audit the AI / EiPE / Prompt Problem (see worksheet)
Key takeaways
- Authenticate once, authorize every request
- Never trust client-supplied IDs or tokens blindly
- Deny by default
Questions?
Next week: Reflection & review (midterm prep)