AuthN & AuthZ — OAuth 2.0, OIDC, JWT
Token validation is more than decoding. A source-bounded lesson with worked reasoning, failure analysis and explicit runtime limitations.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- Trace Authorization Code with PKCE and validate an OIDC identity result
- Inspect and verify JWTs under an explicit issuer, audience and algorithm policy
- Choose session storage, revocation and tenant authorization boundaries
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 31 sections
Why this session exists
Authentication establishes an identity under a defined trust policy; authorization decides whether that identity may perform an action on a resource. A successful login does not entitle someone to every project in a tenant. A valid signature does not make a token appropriate for every API. This lesson follows both boundaries from the browser redirect through token verification to an object-level permission check.
By the end, trace Authorization Code + PKCE, distinguish the historical OAuth grants from current security guidance, explain OIDC, inspect a signed JWT without trusting it, and choose a session/token design with an explicit revocation policy. The local exercises use synthetic identities and keys; they do not contact an identity provider or test a real account.
Prerequisites
Bring HTTP redirects, cookies, TLS, JSON, and the distinction between a symmetric MAC and a public-key signature. HTTP and REST are prerequisites, not a claim that the old session numbers identify a cryptography lesson. You do not need a cloud account.
Optional preparation resources
Choose a resource for the question you want to investigate; there is no required viewing list or fixed pre-read duration.
- OAuth overview: optional orientation before the grant-selection and PKCE sections; use the security BCP cited below for the lesson's current security recommendations.
- How OpenID Connect works: an identity-layer introduction to pair with the ID-token validation section, not a substitute for its recipient and issuer checks.
- OWASP JWT for Java cheat sheet: a language-specific companion. Check library-specific examples against your implementation rather than assuming Java code or every recommendation transfers unchanged to Python.
- Optional video searches: OAuth explained simply, JWT security deep dive, and Fireship + OAuth. These are searches, not verified talks, author attributions, viewing recommendations or duration claims. They do not establish that the local JWT exercises implement an OAuth login flow.
Authentication, authorization and the four parties
The passport/boarding-pass analogy separates identity from permission: knowing a passenger's identity does not allocate a seat. In software these decisions can share a service or credential; they need not always involve different tokens or lifetimes. Anonymous callers can also have limited authorization.
OAuth names four roles: resource owner, client, authorization server and resource server. In a delegated photo-printing example, the user is the owner, the printing app is the client, the account service grants access, and the photo API enforces it. The client receives a scoped credential rather than the user's password. In a machine-to-machine grant there need not be a human owner actively signing in.
A bearer token authorizes its holder according to the resource server's policy; theft permits replay unless another mechanism binds its use. The token may be opaque or JWT-shaped. OAuth is a delegated authorization framework, not a standardized statement of user authentication. OIDC supplies that identity contract. A documented provider-specific identity flow can also exist without OIDC: absence of an ID token alone is not proof of a vulnerability. Treating an arbitrary access token as universal login proof is the mistake.
Protocol evolution and grant selection
Use the standards' dates, not an unsupported adoption timeline. RFC 6749 defined OAuth 2.0 in 2012; OIDC Core 1.0 standardized an identity layer; RFC 7636 defined PKCE in 2015; RFC 8725 supplied JWT best current practices in 2020; RFC 9700 updated OAuth security guidance in January 2025. OAuth 2.1 drafts are not needed to justify these recommendations.
- Authorization Code + PKCE: interactive authorization through the browser, followed by a token-endpoint exchange. Use it for a browser/native application supported by the provider, with the client authentication appropriate to that client type.
- Client Credentials: the client acts on its own behalf or under previously arranged access. It is for confidential clients, not a secret pasted into downloadable JavaScript. Authentication can use a secret, an asymmetric method or another supported mechanism; it is not invariably a static password.
- Refresh token: a credential used at the authorization server to obtain fresh access tokens. It is not a resource-server token and is not one of the original four authorization grant types. Retain only when the application needs offline/continued access, and protect it according to the client architecture.
- Historical implicit and resource-owner-password grants: understand them to migrate legacy systems. RFC 9700 says clients should not use implicit access-token responses, and the password grant must not be used. Code interception defenses do not justify collecting the user's provider password.
Authorization Code plus PKCE, step by step
- Create a fresh high-entropy verifier, an S256 challenge, and transaction state. Store them bound to the initiating browser session. For OIDC, bind the nonce when used as required by the flow/profile. Do not log them.
- Redirect to the configured authorization endpoint with client ID, exact registered redirect URI, response type
code, requested scope and PKCE challenge/method. OIDC requests includeopenid; consent and authentication occur at the provider, not inside the client application's password form. - Receive a short-lived code at the redirect endpoint. Reject unexpected transaction state, issuer/mix-up or redirect mismatches; handle denial as denial, not as an empty successful login.
- Exchange the code over TLS at the token endpoint with the original verifier, redirect URI and required client authentication. A public native/SPA client cannot safely embed a reusable client secret. A confidential backend must still protect its credentials; PKCE does not replace that authentication.
- Validate the returned identity result under the provider contract, establish the application's session, and present the access token only to its intended resource server. Token issuance is not an object-level permission check.
Figure text equivalent — labels and relationships
- Participant “Browser”.
- Participant “Client backend”.
- Participant “Authorization server”.
- Participant “Resource API”.
- 1. solid: “Browser” to “Client backend” — Begin login.
- 2. solid: “Client backend” to “Client backend” — Bind state, verifier and nonce to session.
- 3. dotted: “Client backend” to “Browser” — Redirect with S256 challenge.
- 4. solid: “Browser” to “Authorization server” — Authenticate and authorize requested scope.
- 5. dotted: “Authorization server” to “Browser” — Redirect with code and state.
- 6. solid: “Browser” to “Client backend” — Callback.
- 7. solid: “Client backend” to “Client backend” — Validate transaction binding.
- 8. solid: “Client backend” to “Authorization server” — Code, verifier and client authentication over TLS.
- 9. dotted: “Authorization server” to “Client backend” — Tokens under negotiated profile.
- 10. solid: “Client backend” to “Resource API” — Access token for this API.
- 11. solid: “Resource API” to “Resource API” — Verify token and authorize object/action.
- 12. dotted: “Resource API” to “Client backend” — Allowed response or denial.
Why two steps rather than a token in a redirect?
Both channels should use TLS; the browser channel is not inherently unencrypted. The distinction is exposure and binding. Redirect parameters can reach histories, logs or other unintended consumers; fragments are not sent as HTTP Referer fragments, so the original claim that every fragment automatically enters every proxy log was wrong. A single-use code reduces exposed credential power, while PKCE binds redemption to the initiator's verifier. A code without its verifier cannot be redeemed by a thief under the correct protocol. PKCE does not protect against an attacker who steals both, compromises the client runtime, or controls the redirect endpoint.
RFC 9700 requires CSRF protection. A client that has ensured server PKCE support can rely on the specified PKCE protection; OIDC nonce and session-bound one-time state have defined roles. Do not replace a maintained library's flow checks with a home-grown claim that PKCE solves every redirect attack. Exact redirects, issuer binding, transaction state and account linking are distinct controls.
Offline PKCE worked example
This is RFC 7636 Appendix B's public test vector, not a real credential. It shows base64url without padding and proves that changing the verifier changes the challenge. It is not an OAuth server.
import base64
import hashlib
def challenge(verifier):
return base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode('ascii')).digest()
).rstrip(b'=').decode('ascii')
verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
expected = 'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM'
assert challenge(verifier) == expected
assert challenge(verifier + 'x') != expected
print('RFC 7636 S256 vector: PASS')Callback binding and key rotation are separate state machines
For one login transaction, retain the expected authorization-server issuer, exact redirect URI, browser-session binding, expiry, one-time state and PKCE verifier on the trusted side appropriate to the architecture. Generate fresh values for each attempt rather than reusing a global state string. RFC 9700 requires exact redirect matching, with a specified native-loopback port exception; it does not permit arbitrary suffix matches such as “ends with example.com.” Do not take the callback's query parameters as authority to choose an arbitrary token endpoint. Resolve issuer/endpoints from trusted configuration/discovery and bind them before starting the transaction. Avoid open redirectors and credentials in logs.
The following transaction predicate model uses invented public fixture strings. It models one-time state, session, issuer and expiry checks only. It intentionally does not generate secrets, parse an HTTP callback, exchange a code, validate a redirect registration or implement the PKCE server. A maintained OAuth/OIDC client owns those protocol operations. In production, consuming a transaction must be atomic across concurrent callbacks, with a bounded recovery policy if the subsequent token exchange fails.
# exact-program: callback_binding
def consume_transaction(tx, session, state, issuer, now):
if tx['used'] or now >= tx['expires']:
raise ValueError('used or expired transaction')
if (session, state, issuer) != (tx['session'], tx['state'], tx['issuer']):
raise ValueError('callback binding mismatch')
tx['used'] = True
return tx['redirect']
if __name__ == '__main__':
tx = {'session': 'fake-browser', 'state': 'fake-state',
'issuer': 'https://issuer.example',
'redirect': 'https://client.example/callback',
'expires': 100, 'used': False}
assert consume_transaction(tx, 'fake-browser', 'fake-state',
'https://issuer.example', 99) == tx['redirect']
try:
consume_transaction(tx, 'fake-browser', 'fake-state',
'https://issuer.example', 99)
except ValueError:
pass
else:
raise AssertionError('replayed transaction accepted')
print('one-time callback binding model: PASS')Key-selection trace: configure issuer A's permitted algorithms and trusted JWKS location before examining kid. Treat kid only as an untrusted lookup hint in A's key set, not as a file path or network destination. An unknown key ID may trigger a bounded, rate-limited refresh from that configured issuer; it must not trigger unrestricted token-directed fetches or fallback to another issuer's key. If no valid trusted key is available, reject the token. After cryptographic verification, still enforce issuer, audience, times, token type and object permission.
For planned rotation, the issuer can publish the new public key before signing with it, then overlap old and new verification keys for the permitted token lifetime, clock skew and distribution/cache delays. On compromise, removing the old key from the issuer is insufficient while verifiers retain it in caches; define emergency invalidation and a measurable revocation latency. Stopping refresh-token issuance and revoking an access token are different decisions. A JWT denylist/version check or introspection can shorten access-token revocation latency but adds current state and availability dependencies.
Browser failure exercise: an HttpOnly session cookie blocks a script from reading the cookie value, not from sending same-origin requests carrying it. Use XSS prevention and server-side permission checks as well as CSRF controls for cookie-authenticated state changes. SameSite and CORS address different browser rules; neither is an object-authorization layer. A same-site sibling origin is not automatically trustworthy. For an OAuth callback, choose cookie SameSite behavior compatible with the provider's response method and validate transaction binding rather than relaxing CSRF controls across the whole application.
OIDC: identity proof has an intended recipient
An ID token contains a provider assertion about authentication for the client. Validate issuer, signature under trusted issuer keys, audience containing your client ID, expiration and any nonce/authorized-party rules required by the profile. Use the (issuer, subject) pair as the identity key; a display name or mutable email address is not a universal stable identifier. If using UserInfo, check its sub against the ID token as OIDC requires.
The access token is for the resource API. A client should treat it according to the provider's contract rather than assume it is a parsable JWT. The resource server may validate a documented JWT access-token profile or introspect an opaque token. An ID token with your client ID in aud must not be substituted for an API access token with a different audience.
Worked rejection: a valid issuer signature on a token with audience photo-api does not authorize payroll-api. The signature says who protected the bytes; the audience says who should accept them. Even a correctly addressed projects.read token must still pass the tenant/project check.
JWT anatomy and verification policy
A compact signed JWS JWT has base64url(header).base64url(payload).signature. Header and payload are readable encodings, not encryption. The signature/MAC covers the encoded header and payload together. The kid helps select a key only inside an already trusted issuer/key set; it is not a URL the application should blindly fetch. JWT also permits encrypted JWE forms, so “every JWT is three readable parts” is false.
Common registered claims include iss, sub, aud, exp, nbf, iat, and jti. RFC 7519 does not require every registered claim in every JWT. A particular application profile decides which are required, their meaning, allowed skew and maximum lifetime. exp is a NumericDate in seconds; iat records issue time, not an authorization policy by itself. A scope string is a profile convention, not a substitute for current object permissions.
Six footguns and their defenses
- Unsecured/algorithm-confused tokens: configure allowed algorithms independently of the untrusted header and keep each key tied to its intended algorithm. Do not accept
noneon this signed-token path or mix RSA public keys into an HMAC path. No universal “every library fixed this after year X” assurance exists. - Weak symmetric keys: HS256 needs suitably strong random secret material and secure lifecycle handling. A public demo string is not a deployment key, however long it looks. Do not infer brute-force time from an invented benchmark.
- Missing or excessive validity: require the profile's time claims, reject expiration, enforce any intended maximum age, and define clock skew. Short lifetime limits exposure but does not prevent misuse during that lifetime.
- Wrong issuer, audience or token type: validate all relevant profile bindings, not only a signature. Separate ID-token and access-token acceptance rules to prevent substitution.
- JavaScript-readable storage: localStorage and sessionStorage are readable by compromised same-origin JavaScript. A CSP reduces some attack paths; it is not a guarantee against malicious code already executing in the origin.
- Stale authorization: a signed tenant role can outlive a role change. Evaluate revocation and current policy when the action requires it, especially at tenant/object boundaries.
Sessions, browser storage and revocation
An opaque session ID in a cookie points to server-maintained state. It makes centralized invalidation straightforward, but replicas/caches must observe deletion before revocation takes effect. A shared store is one design, not a requirement to use Redis. Regenerate identifiers at authentication and privilege changes; expire and protect sessions.
A JWT access token can be checked without a per-request session lookup, but trusted keys, clock policy, revocation and changing permissions still create state. It scales differently, not trivially or universally better. A denylist, version check, introspection, or removal of an accepted signing key can revoke tokens before exp; that sacrifices some statelessness. Deleting a refresh-token row prevents future refresh, not immediate rejection of already issued access tokens.
For a same-site web application, a backend-for-frontend can retain OAuth tokens server-side while the browser holds an HttpOnly; Secure session cookie with appropriate SameSite behavior. Browser refresh does not inherently lose that server-side session. HttpOnly prevents direct cookie reads by JavaScript; an XSS attacker may still issue requests as the victim. Automatic cookie attachment also requires CSRF defenses. SameSite is defense in depth, not a universal guarantee against every same-site or application-level attack. Cross-origin credentialed access needs a carefully constrained CORS policy.
A native app normally uses provider-supported code + PKCE, short-lived access tokens and a protected refresh credential in OS-appropriate secure storage. Android Keystore protects keys; it is not simply a universal arbitrary-token vault. Choose a secure-storage library backed by the platform's documented mechanisms. RFC 9700 requires public-client refresh tokens to use rotation or sender constraint to address replay; a device-attestation claim alone is not proof that the presenter holds the original device key. Example lifetimes such as 5–15 minutes are policy choices, not standards requirements.
Hands-on: issue, inspect and reject a synthetic JWT
The following retained teaching program runs only locally using installed PyJWT. The literal key is public fixture data and must never secure real identities. This is a JWT lab, not an OAuth authorization-code implementation. The peek result is explicitly unverified. The five required registered claims plus custom scope total six payload fields, not the original “seven required claims.”
"""
auth_demo.py — Issue and verify a JWT, decode it by hand, and demonstrate
three common footguns.
Run:
pip install "pyjwt[crypto]"
python auth_demo.py
"""
from __future__ import annotations
import base64
import json
import time
from dataclasses import dataclass
import jwt # PyJWT
# ---------- config ----------
ISSUER = "https://auth.example.com"
AUDIENCE = "https://api.example.com"
HS_SECRET = "not-a-real-secret-please-use-256-bits-from-os.urandom" # for HS256
# ---------- issue a token ----------
def issue_token(user_id: str, scopes: list[str], ttl_seconds: int = 300) -> str:
now = int(time.time())
payload = {
"iss": ISSUER,
"sub": user_id,
"aud": AUDIENCE,
"iat": now,
"exp": now + ttl_seconds,
"scope": " ".join(scopes),
}
return jwt.encode(payload, HS_SECRET, algorithm="HS256", headers={"kid": "hs-1"})
# ---------- verify a token (the right way) ----------
def verify_token(token: str) -> dict:
return jwt.decode(
token,
HS_SECRET,
algorithms=["HS256"], # PIN the algorithm — prevents alg confusion
audience=AUDIENCE, # check aud
issuer=ISSUER, # check iss
options={"require": ["exp", "iat", "sub", "aud", "iss"]},
)
# ---------- decode without verifying (for eyeballing only) ----------
def peek(token: str) -> dict:
def _b64d(seg: str) -> dict:
pad = "=" * (-len(seg) % 4)
return json.loads(base64.urlsafe_b64decode(seg + pad))
h, p, _ = token.split(".")
return {"header": _b64d(h), "payload": _b64d(p)}
# ---------- footgun demos ----------
def footgun_alg_none(token: str) -> None:
"""Old libs accepted alg:none. Show that we don't."""
header, payload, _ = token.split(".")
payload_json = json.loads(base64.urlsafe_b64decode(payload + "==="))
payload_json["scope"] = "admin" # attacker escalates
payload_json["sub"] = "attacker"
new_payload = base64.urlsafe_b64encode(json.dumps(payload_json).encode()).rstrip(b"=").decode()
new_header = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').rstrip(b"=").decode()
forged = f"{new_header}.{new_payload}."
try:
verify_token(forged)
print("❌ alg:none accepted — you have a critical vuln")
except jwt.InvalidAlgorithmError:
print("✅ alg:none rejected — algorithm is pinned")
except jwt.DecodeError as e:
print(f"✅ forged token rejected: {e}")
def footgun_expired(user_id: str) -> None:
token = issue_token(user_id, ["read"], ttl_seconds=-1) # already expired
try:
verify_token(token)
print("❌ expired token accepted")
except jwt.ExpiredSignatureError:
print("✅ expired token rejected as it should be")
def footgun_wrong_audience(user_id: str) -> None:
now = int(time.time())
token = jwt.encode(
{"iss": ISSUER, "sub": user_id, "aud": "https://other-api.example.com",
"iat": now, "exp": now + 60, "scope": "read"},
HS_SECRET, algorithm="HS256",
)
try:
verify_token(token)
print("❌ wrong-audience token accepted")
except jwt.InvalidAudienceError:
print("✅ wrong-audience token rejected")
# ---------- happy path ----------
def main() -> None:
token = issue_token("user-42", ["read", "write"])
print("=== issued token ===")
print(token)
print("\n=== decoded (header + payload) ===")
print(json.dumps(peek(token), indent=2))
claims = verify_token(token)
print("\n=== verified claims ===")
print(json.dumps(claims, indent=2))
print("\n=== footgun tests ===")
footgun_alg_none(token)
footgun_expired("user-42")
footgun_wrong_audience("user-42")
if __name__ == "__main__":
main()Anatomy and expected results
issue_token builds the local profile. verify_token pins HS256 and requires the intended issuer/audience and claim presence. PyJWT's require checks presence, while the corresponding claim validators check values; requiring a field alone does not validate every business meaning. peek demonstrates readability without the key. The three negative cases must reject an unsecured forged token, an expired token and a token for another audience. Exact token bytes and wall-clock times vary; assert acceptance/rejection, not a hard-coded serialized token.
Key rotation does not invalidate old tokens until validators stop accepting the old key, including in local caches. For planned rotation, overlap keys until legitimate tokens expire; for compromise, define emergency removal and the resulting availability cost. Forcing logout solely by rotating one shared HMAC key affects every token on that key.
RSA extension: public verification, private signing
With RS256, the issuer signs using a private key and verifiers receive only the public key. A party capable of verifying cannot automatically mint tokens, unlike a holder of the shared HMAC secret. OIDC supports multiple algorithms and key-distribution arrangements; RS256 and a literal /.well-known/jwks.json URL are not universal. Discover and validate the provider's configured jwks_uri under its metadata contract.
This complete local extension creates an ephemeral test-only key in memory. It writes no key files, prints no keys and contacts no provider. RSA key generation is a bounded CPU exercise, not a key-management tutorial.
import time
import jwt
from cryptography.hazmat.primitives.asymmetric import rsa
private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public = private.public_key()
now = int(time.time())
payload = {'iss': 'https://issuer.example', 'aud': 'demo-api',
'sub': 'fixture-42', 'iat': now, 'exp': now + 60}
token = jwt.encode(payload, private, algorithm='RS256', headers={'kid': 'fixture'})
claims = jwt.decode(token, public, algorithms=['RS256'],
audience='demo-api', issuer='https://issuer.example',
options={'require': ['iss', 'sub', 'aud', 'iat', 'exp']})
assert claims['sub'] == 'fixture-42'
try:
jwt.decode(token, public, algorithms=['RS256'], audience='other-api',
issuer='https://issuer.example')
except jwt.InvalidAudienceError:
pass
else:
raise AssertionError('wrong audience accepted')
print('RSA signature and audience checks: PASS')Production failure analysis and tenant authorization
Consider three scenarios, not unsupported company anecdotes. First, a verifier accepts an attacker-chosen algorithm; the fix is a closed verification policy, trusted key selection and negative tests. Second, a stolen redirect code is replayed; PKCE, exact redirects, single use and transaction binding address different parts of the flow. Third, injected JavaScript steals a long-lived token; token storage, XSS prevention and bounded privileges must work together. No one header makes account takeover “near-impossible.”
For multi-tenant SaaS, bind the authenticated subject to a permitted tenant and look up the requested project inside that tenant. Coarse scope answers whether the client may invoke the API class; object policy answers whether this subject may read/edit this project. Keep changing per-project ACLs in the authoritative policy store unless the token design explicitly versions and revokes them. A cached authorization decision needs subject, tenant, object, action and policy-version dimensions plus a revocation contract.
# Pure policy fixture: no tokens or external accounts.
projects = {('alpha', 'p1'): {'viewer': {'u1'}, 'editor': {'u2'}},
('beta', 'p1'): {'viewer': {'u3'}, 'editor': set()}}
def allowed(subject, tenant, project, action, scopes):
required = {'read': 'projects.read', 'edit': 'projects.write'}.get(action)
if required is None or required not in scopes:
return False
acl = projects.get((tenant, project))
if acl is None:
return False
return subject in acl['editor'] or (action == 'read' and subject in acl['viewer'])
assert allowed('u1', 'alpha', 'p1', 'read', {'projects.read'})
assert not allowed('u1', 'beta', 'p1', 'read', {'projects.read'})
assert not allowed('u1', 'alpha', 'p1', 'edit', {'projects.write'})
assert not allowed('u2', 'alpha', 'p1', 'delete', {'projects.write'})
assert not allowed('u2', 'alpha', 'p1', 'edit', set())
print('tenant, action and scope policy: PASS')Recall and worked answers
1. Distinguish authentication and authorization
Authentication establishes the asserted identity under the configured trust policy. Authorization permits or denies this action on this object; it still runs after authentication and can also define anonymous permissions. They may share infrastructure but are not the same decision.
2. Trace code plus PKCE in five steps
Bind verifier/state to the session; send the S256 challenge in the authorization request; receive code after provider authentication/consent; validate callback binding and redeem code with verifier/client authentication; validate identity and use the access token only at its intended API. The offline vector checks the challenge calculation, not the whole flow.
3. What does OIDC add?
A standardized authentication result including an ID token, validation rules and optional UserInfo. Check issuer, audience, signature, times and flow-specific nonce/authorized-party rules. name and email need not appear in every ID token; do not infer them from an arbitrary access token.
4. Which claims does this JWT profile require?
iss, sub, aud, iat, exp; this fixture also carries scope. Explain issuer trust, subject identity, intended recipient, issue time and expiration. These are this profile's requirements, not a universal claim that RFC 7519 mandates all five for every JWT.
5. Name three misuse cases
Accepting none or a wrong algorithm, using a weak/shared demo secret, and omitting recipient/time checks. JavaScript-readable long-lived tokens and stale per-object permissions are additional problems. Decoding base64url is not verification.
6. Compare sessions and JWTs on revocation and scaling
A session lookup can observe a revoked record; replication/cache delay still matters. A local JWT verifier needs an additional state check or key change for early revocation, otherwise it waits for expiration. Distributed sessions and JWT key distribution both have operational costs. Choose from required revocation latency and trust boundaries, not fleet size alone.
7. What threat does PKCE address?
A stolen authorization code alone cannot satisfy the verifier challenge. It does not authenticate arbitrary user identity, prevent all XSS, or make a public client capable of keeping a static secret. RFC 9700 requires public-client use and recommends it for confidential clients.
8. Design a mobile token strategy
Use code + PKCE with supported redirect handling, narrowly scoped short-lived access tokens, and refresh tokens only if needed. Protect refresh credentials in platform-backed storage; rotate or sender-constrain them according to the provider. Reuse detection revokes the affected token family, while already issued access tokens require a separate early-revocation decision. Pick lifetimes from risk and UX, not an asserted universal 30-day rule.
9. Prevent algorithm confusion
Select trusted issuer keys and their allowed algorithms before evaluating the token's request. Never interpret an RSA public key as an HMAC secret. Pin a permitted set with key-to-algorithm binding; a standards-compliant profile may support more than one algorithm during migration without accepting arbitrary ones.
10. Design tenant roles and project overrides
Keep stable subject/tenant context and coarse scopes in the validated profile, then enforce tenant membership and current object ACLs server-side. The fixture proves a valid projects.read scope cannot cross from alpha to beta and a viewer cannot edit. Version or invalidate cached policy results when ACLs change; short token lifetimes are not an immediate permission-revocation guarantee.
Explain-out-loud test
Explain why the wrong-audience token is rejected despite a correct signature, why an HttpOnly cookie does not cure XSS-driven requests, and why deleting a refresh record does not revoke an issued access token. A complete answer names the verification profile and the object authorization boundary.
Where this connects next
HTTP supplies redirects/cookies/TLS, REST and GraphQL need object/field authorization, and RPC services need service identity plus permission checks. Web-security work supplies XSS/CSRF defenses; system-design reviews should specify session expiry, revocation latency and tenant isolation explicitly. These are dependencies, not an unsupported claim about what every company uses.
Continue the sequence with S060 · OS basics: processes, threads, memory and file descriptors. Hub: The 6-Month Learning Plan.
Suggested study route: identity/permission intuition → PKCE flow visual → synthetic local exercises → browser and tenant failure analysis → recall. Pause until you can explain each rejection without decoding a token as proof of trust. Work at your own pace: this is not a timed completion promise or a claim to have implemented provider login or production authorization.
Sources and execution boundary
Reviewed against RFC 9700 (January 2025), RFC 7636 (September 2015), RFC 7519 (May 2015), RFC 8725 (February 2020), OIDC Core 1.0 including errata, PyJWT 2.7.0 tagged implementation, and OWASP guidance captured on 2026-10-08. Earlier primary snapshots and exact passages are retained in the batch5 evidence; the 10 October body review revalidates that provenance and records fresh local checks separately. Local JWT, RSA, PKCE and policy checks do not reproduce browser redirects, provider login, JWKS rotation, secure storage, CSRF defenses or a deployed authorization service.
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.