AuthN vs AuthZ, Sessions & Password Storage
Separate credential validation from permission decisions; reason about password storage, sessions, JWTs, revocation, tenant isolation and safe local tests.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- Validate identity and enforce tenant-object-action authorization on protected operations
- Choose password hashing, throttling and session or token lifecycle controls
- Execute negative tests for identity lookup, expiry, revocation boundaries and policy
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 15 sections
A valid credential answers an identity question. It does not automatically grant access to a particular tenant, document or operation. The difficult part of authentication and authorization is preserving that distinction through login, token validation, storage, background work, revocation and failure.
Preparation and optional resources
Start with HTTP cookies/headers and database keys. The old prerequisite labels “S062 REST & HTTP” and “S068 Databases” were numbering cues, not reliable current routes; the required concepts are unchanged. Use this session to distinguish credential verification, permission checks and state freshness before implementing an adapter.
The following are restored discovery pointers, not additional governing evidence. Video titles are historical finding labels; viewing, creator attribution, duration, external availability and quality were not verified. Search links are searches, not a selected recording.
- Authentication vs Authorization — optional intuition for the two decisions; compare it with the request-boundary explanation below.
- Session vs JWT auth — optional preview of storage, validation and revocation tradeoffs, not a rule that every client needs JWTs.
- JWT internals search — discover a walkthrough; check algorithm policy, claim validation and revocation against the pinned RFCs here.
- OWASP Authentication Cheat Sheet — preparation on login, throttling, recovery and enumeration signals; the governing evidence below is the dated snapshot, not an unversioned API default.
- OWASP Password Storage Cheat Sheet — compare Argon2id, scrypt and legacy bcrypt parameters with the explicitly dated profiles below.
- Pwned Websites index — optional incident discovery only. The index does not establish that every breach resulted from these authentication mistakes; do not submit account information for this exercise.
Identity first, permission at every protected boundary
A hotel key card is a useful analogy: check-in establishes a guest identity, while each door decides whether that card may open that room. A copied bearer card can be used by another person; card possession does not prove the human's current presence. Revocation works only when the reader's policy and state reflect it.
Authentication (AuthN) establishes a principal at a stated assurance level. Password, federated login, magic link and WebAuthn have different threat models; MFA can raise assurance, and sensitive actions may require recent reauthentication. Authorization (AuthZ) decides whether that principal may perform an action on an object in a context. Validate the session/token on each protected request; do not interpret “AuthN once” as checking credentials only at login and trusting subsequent requests blindly.
Figure text equivalent — labels and relationships
- Node “Login credential proof” (square).
- Node “Issue session or access token” (square).
- Node “Protected request” (square).
- Node “Validate credential and recover principal” (square).
- Node “Action plus tenant plus object policy” (square).
- Node “Perform operation” (square).
- Node “Deny without disclosing protected data” (square).
- “Login credential proof” leads to “Issue session or access token”. Line: normal; ends: arrow point.
- “Issue session or access token” leads to “Protected request”. Line: normal; ends: arrow point.
- “Protected request” leads to “Validate credential and recover principal”. Line: normal; ends: arrow point.
- “Validate credential and recover principal” leads to “Action plus tenant plus object policy”. Line: normal; ends: arrow point.
- “Action plus tenant plus object policy” leads to “Perform operation”: allow. Line: normal; ends: arrow point.
- “Action plus tenant plus object policy” leads to “Deny without disclosing protected data”: deny. Line: normal; ends: arrow point.
Reject unauthenticated access according to the protocol, commonly HTTP 401 with the applicable challenge; authenticated-but-forbidden access commonly uses 403. A service may return 404 to avoid disclosing whether a protected object exists. These statuses are not substitutes for a policy. A frontend hiding “Delete User” is user experience, not authorization: alternate clients can call the backend directly.
The OWASP authorization guidance recommends least privilege, deny by default and validation on every request. Include object downloads, exports, caches, GraphQL resolvers, batch jobs and admin tools. Do not trust a caller-provided X-User-Id unless a separately authenticated, tightly controlled gateway strips incoming versions and establishes that header under an explicit trust contract.
RBAC, tenant roles, ABAC and relationships
RBAC maps roles to permissions. Tenant-scoped roles add (user, workspace, role) membership: Alice can be an editor in A and a viewer in B. ABAC evaluates attributes of subject, resource, action and context; relationship-based models express ownership/sharing relationships. A policy engine can centralize evaluation, but OPA/Cedar-like engines and Zanzibar-style relationship systems are not interchangeable implementation labels.
For a document read, establish the principal from a verified credential, load its current membership, bind the document to that same tenant, evaluate the action, and fetch only allowed data. Tenant-scoped queries or database row-level security reduce forgotten checks, but neither makes bypass impossible: privileged connections, alternate query paths, caches and policy configuration remain part of the boundary. An if can be a correct authorization check; a WHERE clause can be wrong. Test the invariant, not the syntax style.
A UUID makes casual enumeration harder but is not a permission. IDs leak through links, logs, exports and shared screens. Test user A reading B's object, a removed member, a viewer attempting a write, a cross-tenant administrator and a valid token with stale claims. Include negative tests for every access path rather than only the owner's successful request.
Password storage: slow verification with bounded resource use
Passwords must be available transiently to verify or hash, but must not be durably stored or logged in plaintext. Use an established password-hashing library with a unique randomly generated salt, encoded algorithm/parameters and a calibrated work factor. Salt is not secret; it prevents identical password hashes and amortized precomputation, not online guessing or password reuse. A fast hash such as plain SHA-256 remains unsuitable even when salted. Encryption is reversible and is generally the wrong password-storage primitive.
The OWASP password-storage sheet, captured for this review, recommends Argon2id for new systems with a minimum configuration of 19 MiB, two iterations and parallelism one. RFC 9106 (2021) supplies different recommended Argon2id profiles, including a memory-constrained 64 MiB, three-pass, four-lane profile. These are different documented baselines, not contradictory defaults to silently mix. Benchmark on the deployed hardware under concurrent login load, enforce input limits and plan rehash-on-success as the policy changes.
Where Argon2id is unavailable, OWASP gives scrypt guidance; bcrypt is primarily a legacy choice with cost at least 10 and a 72-byte input limit. Cost 12 means an exponential work parameter, not a portable 250 ms promise. UTF-8 characters can occupy multiple bytes. Reject over-limit inputs explicitly with the pinned bcrypt behavior rather than silently truncating two different passwords to the same first 72 bytes. Do not concatenate a hand-made salt onto the password; use gensalt/library APIs. A stored bcrypt string already encodes version, cost and salt.
Registration:
validate password policy and byte limits -> library creates salt/hash
-> store encoded hash with stable user ID; never log the password
Login:
rate-limit before expensive work -> select stored or precomputed dummy hash
-> library verifies -> generic failure or issue a fresh session
-> rehash after success if current policy requires different parametersA precomputed valid dummy hash makes unknown-user requests perform comparable password work. The original all-zero dummy encoding was not a trustworthy bcrypt hash, and if not user or verify(...) short-circuited the unknown-user path. Comparable work reduces one enumeration signal; it does not prove constant-time HTTP responses. Database hits, responses, registration/recovery flows, network noise and rate limiting can still distinguish accounts. Do not claim a timing side channel is eliminated because one hash call is constant-time.
Rate limits, lockout, recovery and MFA
Use layered account, source/device and global resource controls. Per-IP limits can penalize shared networks and be bypassed by distributed sources; per-account lockout can be abused to deny a victim access. Bounded backoff, challenge/escalation, alerting and recovery must balance those risks. Counters must be shared/atomic across production workers, expire safely and have bounded key growth. “Five attempts per five minutes” is an exercise parameter, not proof that most credential stuffing is stopped.
Use generic login/reset responses, TLS, appropriate MFA/passkeys, breached-password screening where policy requires it and an audited recovery flow. Throttle before expensive hashing to bound CPU, but retain enough signal to detect attacks without storing passwords or tokens. Password reset must not silently grant new roles; revoke or reassess sessions according to the security policy.
Sessions, JWTs and where state lives
A server-side session uses an opaque unpredictable ID referencing an identity, expiry and other state. Cookies are one transport, not a token format: a JWT may also travel in a cookie, and an opaque token can travel in a header. Generate session IDs with a CSPRNG; secrets.token_urlsafe(32) draws 32 bytes of entropy. A properly generated UUIDv4 is not inherently predictable, but a dedicated session-token API and a documented entropy policy are clearer.
Figure text equivalent — labels and relationships
- Participant “Client”.
- Participant “Application”.
- Participant “Session or policy store”.
- 1. solid: “Client” to “Application” — Login proof.
- 2. solid: “Application” to “Session or policy store” — Create fresh session.
- 3. dotted: “Application” to “Client” — Opaque cookie.
- 4. solid: “Client” to “Application” — Protected request with cookie.
- 5. solid: “Application” to “Session or policy store” — Validate session and current policy.
- 6. dotted: “Application” to “Client” — Allowed data or denial.
- 7. note: “Client” to “Application” — Alternative signed access-token request (note placement over).
- 8. solid: “Client” to “Application” — Bearer JWT.
- 9. solid: “Application” to “Application” — Verify algorithm, signature and required claims.
- 10. solid: “Application” to “Session or policy store” — Current entitlement or revocation check if policy requires it.
- 11. dotted: “Application” to “Client” — Allowed data or denial.
Session tradeoff: revocation is straightforward when each request consults authoritative state. Caches, replicas and in-flight requests can add delay; “delete row means globally instant” is too strong. The store becomes an availability and latency dependency. Permission freshness also requires loading current policy, not indefinitely caching roles inside a session.
JWT tradeoff: a signed JWT (JWS) can be validated locally by appropriate keys and policy. Its payload is readable; signing does not encrypt. JWTs can also be encrypted in other profiles, so “JWT means signed plaintext” is not a complete format definition. Local signature validation alone does not consult revocation state and does not make old roles current. Validate algorithm/key compatibility, issuer, audience, expiry, not-before where used, token type and required claims for the application. Do not choose an algorithm from an untrusted header or accept an ID token as an arbitrary API access token. RFC 8725 (2020) covers these validation obligations.
Hybrid tradeoff: short-lived access tokens plus revocable refresh credentials separate frequent API access from renewal. Refresh tokens need not always be opaque or live weeks; that is a design choice. Rotation with reuse detection or sender-constraining protects public-client refresh flows under RFC 9700 (2025). Revoking refresh prevents renewal, not use of an already issued access token. A denylist or user/session-version check only works if every relevant validator consults sufficiently fresh state; no “emergency denylist without checks” magic exists. Key rotation can invalidate a broad class of tokens but has cache/rollout consequences and is not a precise replacement for user revocation.
For a single first-party browser application, an opaque session is often simpler. Many validators or federation may justify JWTs. Mobile does not inherently require JWTs, nor does “microservices” eliminate authorization state. Choose lifetimes from theft/revocation risk and operational capability, not a universal 15-minute/30-day rule.
A request can pass AuthN and still fail AuthZ or CSRF
Work through a cookie-authenticated invoice update. First validate the session and recover Alice's stable identity. Next establish the invoice's tenant and Alice's current write permission; a valid session for a viewer or another tenant must fail. Finally apply the browser request-integrity policy for this state-changing operation: an automatically attached cookie does not prove Alice intended a cross-site submission. A valid CSRF token/origin check cannot grant missing invoice permission, and passing invoice authorization does not replace the CSRF check. These are separate gates, not alternate definitions of authentication.
For a header-bearer API, specify whether browsers can attach credentials automatically and which origins/clients can supply the header. Do not conclude “JWT prevents CSRF”: a JWT in a cookie has the cookie threat model. Nor does HttpOnly defeat script-driven actions after XSS. The lab's Origin and logout-token checks demonstrate a narrow first-party adapter; they do not implement every federation or cross-origin login flow.
JWT signing-key selection is another trust decision: a token's kid can select only from an issuer's trusted configured key set under a fixed algorithm policy. An arbitrary jku/x5u from an untrusted header is not authority to fetch a key. Key rollover needs a bounded overlap for still-valid tokens and a way to stop accepting a compromised old key at every validator; rotating a key does not itself update remote caches. This key lifecycle differs from rotating a session ID or revoking one refresh credential. RFC 8725 §§3.1 and 3.10 govern algorithm checks and untrusted key-related headers.
Cookie and browser protections
OWASP session guidance explains Secure, HttpOnly, narrow scope, renewal and timeout controls. HttpOnly prevents JavaScript from reading a cookie; XSS can still perform actions as the user. Secure restricts transport; SameSite reduces some cross-site sending but does not replace a complete CSRF design. State-changing cookie-authenticated operations need appropriate CSRF tokens and origin validation; login/logout themselves can need protection. Strict versus Lax must fit legitimate navigation/federation flows. The CSRF sheet explicitly treats SameSite as defense in depth in many settings.
Use absolute and idle session timeouts, rotate the identifier on login/privilege change, clear the matching cookie attributes on logout and revoke server state. LocalStorage is script-readable; OS secure storage and HttpOnly cookie patterns have different browser/native threat models. Never log access/refresh/session tokens, and protect backups and crash dumps too.
Executed login and authorization core
The exact program below uses installed bcrypt and PyJWT with in-memory stores and a fixed synthetic clock. It has no listener and no real users. It corrects the original email-versus-ID mismatch, invalid dummy hash, stale-role mutation and missing JWT claim policy. The rate limiter is bounded only for this finite fixture; production storage/concurrency, account recovery and web protections are discussed separately rather than falsely implemented by a few decorators.
import secrets
import bcrypt
import jwt
from dataclasses import dataclass
@dataclass
class User:
uid: str
email: str
password_hash: bytes
roles: frozenset
class AuthLab:
def __init__(self):
self.users = {} # stable user ID -> User
self.by_email = {} # normalized email -> stable ID
self.sessions = {}
self.attempts = {}
self.key = secrets.token_bytes(32) # ephemeral lab key; never printed
self.dummy = bcrypt.hashpw(b"dummy-not-a-login", bcrypt.gensalt(rounds=12))
@staticmethod
def password_bytes(password):
raw = password.encode("utf-8")
if not 1 <= len(raw) <= 72:
raise ValueError("bcrypt byte length outside lab policy")
return raw
def signup(self, uid, email, password, roles=frozenset({"user"})):
email = email.strip().lower()
if uid in self.users or email in self.by_email:
raise ValueError("duplicate identity")
raw = self.password_bytes(password)
if len(password) < 15:
raise ValueError("lab registration requires 15 characters")
self.users[uid] = User(uid, email, bcrypt.hashpw(raw, bcrypt.gensalt(rounds=12)), roles)
self.by_email[email] = uid
def verify_login(self, email, password, now):
email = email.strip().lower()
recent = [t for t in self.attempts.get(email, []) if now-300 < t <= now]
self.attempts[email] = recent
if len(recent) >= 5:
return 429, None
recent.append(now)
user = self.users.get(self.by_email.get(email))
try:
raw = self.password_bytes(password)
except ValueError:
raw = b"invalid-input-sentinel"
valid_input = False
else:
valid_input = True
ok = bcrypt.checkpw(raw, user.password_hash if user else self.dummy)
if not (user and ok and valid_input):
return 401, None
return 200, user.uid
def issue_session(self, uid, now, previous=None):
if uid not in self.users:
raise ValueError("unknown identity")
if previous:
self.sessions.pop(previous, None)
sid = secrets.token_urlsafe(32)
self.sessions[sid] = (uid, now+3600)
return sid
def session_user(self, sid, now):
row = self.sessions.get(sid)
if not row or now >= row[1]:
return None
return self.users.get(row[0])
def logout(self, sid):
self.sessions.pop(sid, None)
def issue_access(self, uid, now):
if uid not in self.users:
raise ValueError("unknown identity")
return jwt.encode({"sub": uid, "iss": "auth-lab", "aud": "api-lab",
"iat": now, "nbf": now, "exp": now+900},
self.key, algorithm="HS256")
def access_user(self, token, now):
# Signature/issuer/audience/required fields use the real library.
# Numeric date checks use the injected fixture clock explicitly.
try:
p = jwt.decode(token, self.key, algorithms=["HS256"],
issuer="auth-lab", audience="api-lab",
options={"require": ["sub", "iss", "aud", "iat", "nbf", "exp"],
"verify_exp": False, "verify_nbf": False, "verify_iat": False})
if not isinstance(p["sub"], str):
return None
if any(type(p[k]) is not int for k in ("iat", "nbf", "exp")):
return None
if not p["iat"] <= now or not p["nbf"] <= now < p["exp"]:
return None
return self.users.get(p["sub"])
except jwt.PyJWTError:
return None
@staticmethod
def allowed(user, action, doc, memberships):
if user is None:
return False
role = memberships.get((user.uid, doc["tenant"]))
if action == "read":
return role in {"viewer", "editor", "admin"}
if action == "write":
return role in {"editor", "admin"}
return False
if __name__ == "__main__":
lab = AuthLab()
lab.signup("u1", "alice@example.test", "synthetic phrase alpha")
lab.signup("u2", "bob@example.test", "synthetic phrase beta")
assert lab.verify_login("alice@example.test", "synthetic phrase alpha", 0) == (200, "u1")
assert lab.verify_login("absent@example.test", "synthetic phrase alpha", 0) == (401, None)
sid = lab.issue_session("u1", 0)
assert lab.session_user(sid, 3599).uid == "u1"
assert lab.session_user(sid, 3600) is None
access = lab.issue_access("u1", 0)
assert lab.access_user(access, 899).uid == "u1"
assert lab.access_user(access, 900) is None
members = {("u1", "A"): "viewer", ("u2", "B"): "admin"}
assert lab.allowed(lab.users["u1"], "read", {"tenant": "A"}, members)
assert not lab.allowed(lab.users["u1"], "read", {"tenant": "B"}, members)
assert not lab.allowed(lab.users["u1"], "write", {"tenant": "A"}, members)
lab.logout(sid)
assert lab.session_user(sid, 1) is None
assert lab.access_user(access, 1).uid == "u1" # session logout is not JWT revocation
print("identity lookup, password verification, expiry and tenant policy: PASS")
# Complete in-process HTTP adapter. Flask 3.0.2 test client opens no listener.
from flask import Flask, request, jsonify
def make_auth_app(lab, clock):
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
csrf_by_session = {}
origin = "https://auth.example.test"
def principal():
sid = request.cookies.get("__Host-sid")
bearer = request.headers.get("Authorization", "")
if sid and bearer: # fail closed on ambiguous credentials
return None
if bearer.startswith("Bearer "):
return lab.access_user(bearer[7:], clock())
return lab.session_user(sid, clock())
@app.post("/login")
@app.post("/login/access")
def login_http():
if request.headers.get("Origin") != origin:
return jsonify(error="origin denied"), 403
body = request.get_json(silent=True)
if not isinstance(body, dict) or not all(isinstance(body.get(k), str) for k in ("email", "password")):
return jsonify(error="invalid input"), 400
status, uid = lab.verify_login(body["email"], body["password"], clock())
if status != 200:
return jsonify(error="invalid credentials" if status == 401 else "try later"), status
if request.path == "/login/access":
response = jsonify(access_token=lab.issue_access(uid, clock()), expires_in=900)
else:
previous = request.cookies.get("__Host-sid")
sid = lab.issue_session(uid, clock(), previous)
csrf_by_session.pop(previous, None)
csrf_by_session[sid] = secrets.token_urlsafe(32)
response = jsonify(csrf=csrf_by_session[sid])
response.set_cookie("__Host-sid", sid, secure=True, httponly=True,
samesite="Strict", path="/", max_age=3600)
response.headers["Cache-Control"] = "no-store"
return response
@app.get("/me")
def me_http():
user = principal()
if user is None:
return jsonify(error="unauthorized"), 401
return jsonify(id=user.uid, email=user.email)
@app.get("/admin")
def admin_http():
user = principal()
if user is None:
return jsonify(error="unauthorized"), 401
if "admin" not in user.roles:
return jsonify(error="forbidden"), 403
return jsonify(ok=True)
@app.post("/logout")
def logout_http():
sid = request.cookies.get("__Host-sid")
supplied = request.headers.get("X-CSRF-Token", "")
expected = csrf_by_session.get(sid)
if (request.headers.get("Origin") != origin or expected is None
or not secrets.compare_digest(expected, supplied)):
return jsonify(error="forbidden"), 403
lab.logout(sid)
csrf_by_session.pop(sid, None)
response = jsonify(ok=True)
response.delete_cookie("__Host-sid", path="/", secure=True, httponly=True, samesite="Strict")
response.headers["Cache-Control"] = "no-store"
return response
return app
if __name__ == "__main__":
http_app = make_auth_app(lab, lambda: 10)
with http_app.test_client() as client:
base = "https://auth.example.test"
headers = {"Origin": base}
assert client.get("/me", base_url=base).status_code == 401
reply = client.post("/login", base_url=base, headers=headers,
json={"email": "alice@example.test", "password": "synthetic phrase alpha"})
assert reply.status_code == 200
cookie = reply.headers["Set-Cookie"]
assert all(flag in cookie for flag in ("Secure", "HttpOnly", "SameSite=Strict", "Path=/"))
assert client.get("/me", base_url=base).get_json()["id"] == "u1"
assert client.get("/admin", base_url=base).status_code == 403
assert client.post("/logout", base_url=base, headers=headers).status_code == 403
assert client.post("/logout", base_url=base,
headers=headers | {"X-CSRF-Token": reply.get_json()["csrf"]}).status_code == 200
assert client.get("/me", base_url=base).status_code == 401
print("Flask test-client session/cookie/origin/CSRF lifecycle: PASS")The synthetic password phrases are public fixtures, not credentials for a service. The generated signing key exists only inside the process and is neither logged nor persisted. HS256 is appropriate only within a trust boundary where verifiers permitted to know the key are also permitted to mint tokens; use a suitable asymmetric profile when those privileges must differ. The explicit fixture-clock checks are not a recommendation to disable time validation in a web service.
From the core to HTTP without an unsafe demo server
The complete in-process Flask adapter above demonstrates login, token issuance, profile/admin access and CSRF-protected logout without a server. Registration uses the explicit core fixture instead of exposing an unauthenticated signup API. A deployed adapter must: parse and bound inputs; invoke verify_login; on success rotate a session and issue a secure scoped cookie; on protected requests recover the principal and evaluate object/action policy; on logout validate CSRF/origin, revoke state and clear the same cookie. A JWT endpoint can return an access token with Cache-Control: no-store; it must not mutate a stored user's roles from token claims. Reject ambiguous requests that supply conflicting session and bearer identities instead of silently choosing one.
POST /signup -> validate policy; create stable ID and email index
POST /login -> rate limit; comparable hash verification; rotate session
POST /login/access -> same verification; issue profile-bound short access token
GET /me -> validate session OR bearer according to endpoint's chosen scheme
GET /admin -> validate principal AND current admin policy
POST /logout -> CSRF/origin check for cookies; revoke session; clear cookie
GET /tenants/A/documents/42 -> membership AND object tenant AND actionThe listed routes combine implemented test-client paths and the remaining signup/tenant adapter requirements; they are not claims of a deployed production auth service. The executed tests exercise the exact core and HTTP session adapter: missing identity, wrong password, expiry boundary, unsigned/wrong-audience/missing-claim tokens, role removal, cross-tenant access and rate-window behavior. No network timing attacks or external IDOR probes were run.
Review the historical lesson without invented breach universals
Password systems evolved from fast legacy crypt schemes toward configurable work factors and memory-hard designs. RFC 9106 identifies Argon2's Password Hashing Competition provenance; the point is migration and parameter agility, not a timeline claiming bcrypt is always the current default. Passkeys/WebAuthn address phishing-resistant public-key authentication, but recovery, session theft and authorization remain.
Auth0's 2015 JWT library vulnerability report explains unsigned-token acceptance and asymmetric/symmetric algorithm confusion. The corrective control is explicit algorithm/key policy and modern validation—not assuming all old Python/Node/Ruby libraries behaved identically. Never treat a successful decode as a verified token unless verification and required claims are actually enforced.
The original LinkedIn/Snapchat narratives mixed secondary sources, exact breach counts and unverified remediation claims. Retain their relevant mechanisms as explicit scenarios: a leaked unsalted fast-hash database permits cheap repeated guesses; an unrestricted contact-discovery endpoint can disclose relationships at scale. Repair with modern password hashing, anti-abuse controls, data minimization and resource/relationship authorization. Those controls do not prove what a named company deployed or that every breach came from only these mistakes.
Multi-tenant design exercise
Use users(id, email, password_hash), workspaces(id) and memberships(user_id, workspace_id, role) with uniqueness constraints. Authenticate at the credential boundary, then enforce tenant/action/object policy at each data path. Evaluate membership changes from a freshness contract; log selected denials and sensitive grants with sanitized principal/resource IDs and retention limits. Key rotation should follow threat/algorithm/lifetime policy with overlap and emergency invalidation testing, not an arbitrary monthly calendar. Recovery/MFA enrollment require reauthentication and anti-takeover controls.
Exercise: remove Alice from workspace A while her access token is valid. The core's next current-membership check denies access; a validator trusting cached roles would not. Then log out the session while keeping an issued access token: the program shows the token remains valid until expiry unless an additional revocation rule rejects it. These are two different state changes.
Recall and worked answers
- AuthN versus AuthZ? Establish principal/assurance; decide action on resource in context.
- Order and frequency? Login establishes a credential; each protected boundary validates it and authorizes the operation. Reauthentication may be required for risk-sensitive actions.
- Why salt? Prevent identical hashes and amortized precomputation; salt does not make weak passwords strong.
- Why not SHA-256? A fast hash is cheap to guess offline; password KDFs intentionally spend CPU/memory with calibrated parameters. No universal milliseconds or cracking guarantee applies.
- Sessions versus JWT? Central state simplifies revocation; local token checks reduce some round trips but introduce stale claims and revocation constraints. Choose by trust boundaries and freshness needs, not client-device stereotypes.
- Revoke before JWT expiry? Consult a fresh denylist/introspection/session-version policy or reject affected keys under a deliberate rotation strategy. Short expiry limits exposure but is not immediate revocation.
- Frontend-only policy? A client can bypass it. Enforce tenant/action/object rules in trusted services/data paths and test negatives.
- Three login protections? Layered throttling, comparable password verification with generic responses, and TLS plus session/CSRF controls. Add MFA/recovery controls to the threat model; cookies alone do not defeat XSS/CSRF.
- Refresh token lifetime? Renewal credential lifetime/format is a policy choice; protect storage, rotate or sender-constrain as required, detect replay and revoke state. Removing refresh does not revoke issued access tokens.
- Tenant SaaS? Stable identities, workspace memberships, current policy checks, secure session/token lifecycle, audit/privacy rules, recovery, MFA and negative tests across tenants and actions.
Close the page and explain why a valid token for the wrong object must fail, why the dummy hash is real work rather than a string placeholder, and which state change makes logout effective at each validator.
Prerequisites and study route
Bring HTTP headers/cookies, database keys and basic Python. TLS protects credential transport; OAuth/OIDC adds delegated/federated flows; MFA/WebAuthn changes authentication assurance. Authorization, rate limiting and recovery remain necessary in each case.
The route continues to TLS/PKI for credential transport and OWASP risks for authentication versus access-control failures. OAuth/OIDC belongs to delegated/federated identity, MFA/WebAuthn to authentication assurance, rate limiting to abuse/resource budgets, and system-design review to explaining the whole identity/permission lifecycle. The old S084/S085/S089/S128 labels for those subjects were inconsistent with the present catalog; they remain topic directions, not invented destination links.
Optional historical context: the original LinkedIn secondary account and the original Snapchat secondary account preserve the prior incident-reading paths. They do not support the removed exact counts, claimed remediations or breach universals. The Auth0 report remains linked in the historical section and is separately evidence-bound.
Next: S082 — TLS and PKI, to distinguish secure transport from permission. Hub: The 6-Month Learning Plan.
Study pacing: the original pre-read block suggested 30 minutes, separately from its 90-minute session cue; individual resource durations were not verified. The session proposed intuition (5), visual walkthrough (15), hands-on (25), production cases (15) and recall/stretch (10). These are planning cues, not measured completion times; the five named blocks total 70 minutes, leaving 20 minutes of the stated session budget unallocated. Use the safe local fixtures or source-only references as marked, then explain the failure cases without notes. This retains the original 130-session-series context without promising the current catalog is an unchanged evergreen set.
Sources and verification boundary
Primary source passages were read and captured on 2026-10-08, with raw/canonical hashes, exact quote offsets and claim-specific entailment retained in the batch evidence. Historical pins describe their own versions, not current deployment recommendations.
Exact bcrypt 3.2.2/PyJWT 2.7.0 core tests and Flask 3.0.2 in-process client checks were executed with network/process creation blocked. This is not a timing-side-channel or production authentication audit.
- Authorization - OWASP Cheat Sheet Series — Mutable official documentation snapshot accessed 2026-10-08T15:35:49.934762+00:00.
- Password Storage - OWASP Cheat Sheet Series — Mutable official documentation snapshot accessed 2026-10-08T15:35:49.943665+00:00.
- pyca/bcrypt — README.rst (3.2.2) — repository tag 3.2.2.
- Authentication - OWASP Cheat Sheet Series — Mutable official documentation snapshot accessed 2026-10-08T15:35:49.687207+00:00.
- Session Management - OWASP Cheat Sheet Series — Mutable official documentation snapshot accessed 2026-10-08T15:35:49.962103+00:00.
- Cross-Site Request Forgery Prevention - OWASP Cheat Sheet Series — Mutable official documentation snapshot accessed 2026-10-08T15:35:50.386413+00:00.
- RFC 8725, February 2020 — RFC 8725, February 2020.
- RFC 9700, January 2025 — RFC 9700, January 2025.
- RFC 9106, September 2021 — RFC 9106, September 2021.
- jpadilla/pyjwt — jwt/api_jwt.py (2.7.0) — repository tag 2.7.0.
- Critical vulnerabilities in JSON Web Token libraries — Auth0 historical JWT vulnerability report, 2015.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.