Web security has evolved significantly since the release of OAuth 2.0 in 2012. Originally built for an era of monolithic servers and simple mobile apps, OAuth 2.0 relied on patterns (like the Implicit Grant flow) that are now considered insecure due to threat vectors like token interception and browser history exposure.
To patch these architectural security holes, the internet community is moving to **OAuth 2.1**. This upcoming standard consolidates best security practices from the last decade, deprecating vulnerable flows and enforcing modern cryptographical protections. Combined with **OpenID Connect (OIDC)**, it forms the foundation of modern federated identity and API authorization.
1. What’s Changing in OAuth 2.1?
OAuth 2.1 is not a complete rewrite; rather, it is a consolidation of OAuth 2.0 security extensions (such as Security Best Current Practices and PKCE rules) into a single, cleaner specification. Key changes include:
- Enforcing PKCE: Proof Key for Code Exchange (PKCE) is now mandatory for *all* clients using the Authorization Code flow, not just single-page applications.
- Deprecating Implicit Flow: The Implicit Grant flow (which returns access tokens directly in the URL redirect hash) is completely omitted. Single-page applications must use Authorization Code with PKCE instead.
- Deprecating Resource Owner Password Credentials (ROPC): The flow where users input passwords directly into client apps is removed to prevent credentials logging.
- Restricting Redirect URIs: Exact string matching is enforced for redirect URIs; wildcard subdomains and relative paths are no longer permitted to prevent redirect hijacking.
“By making PKCE mandatory, OAuth 2.1 prevents malicious apps on the user’s device from intercepting authorization codes and exchanging them for access tokens.”
2. How PKCE Works Step-by-Step
Proof Key for Code Exchange (PKCE, pronounced “pixy”) introduces a dynamic cryptographic secret generated for each session. This secret ensures that only the specific client that initiated the auth request can complete it at the token endpoint.
The PKCE handshake follows three key steps:
- Code Verifier: The client generates a high-entropy, random cryptographical string (`code_verifier`) using characters `[A-Z]`, `[a-z]`, `[0-9]`, and standard symbols.
- Code Challenge: The client hashes the verifier using SHA-256 and base64url-encodes the result to create the `code_challenge`.
- Verification: The client sends the challenge during the initial redirect. When later exchanging the returned authorization code for tokens, the client sends the raw `code_verifier`. The authorization server hashes the verifier and matches it to the initial challenge. If they match, the tokens are issued.
Here is a basic Javascript script to generate a PKCE code challenge natively in single-page apps:
// Helper to generate a cryptographically secure random verifier
function generateCodeVerifier() {
const array = new Uint32Array(56);
window.crypto.getRandomValues(array);
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
}
// Helper to calculate SHA-256 hash
async function generateCodeChallenge(verifier) {
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await window.crypto.subtle.digest('SHA-256', data);
return btoa(String.fromCharCode.apply(null, new Uint8Array(hash)))
.replace(/+/g, '-')
.replace(///g, '_')
.replace(/=+$/, '');
}
// Execution example
(async () => {
const verifier = generateCodeVerifier();
const challenge = await generateCodeChallenge(verifier);
console.log("Save Verifier in SessionStorage:", verifier);
console.log("Send Challenge to Auth URL:", challenge);
})();
3. OpenID Connect (OIDC) vs. OAuth 2.1
It is critical to distinguish between identity (authentication) and access (authorization).
- OAuth 2.1 is strictly an **authorization framework**. It issues **Access Tokens** (typically opaque strings or JWTs) designed for API Resource Servers. An access token answers the question: *Does the bearer have access scope to edit or read this endpoint?*
- OIDC is an **identity layer** built directly on top of OAuth. It introduces the **ID Token** (always a cryptographically signed JWT). An ID token answers the question: *Who is the user, and when did they authenticate?*
Validating OIDC ID Tokens Natively
To safely trust an ID Token (JWT), your backend must fetch public keys from the provider’s JWKS (JSON Web Key Set) endpoint and verify the signature:
// Python PyJWT ID Token verification
import jwt
from jwt import PyJWKClient
jwks_url = "https://identity-provider.com/.well-known/jwks.json"
jwk_client = PyJWKClient(jwks_url)
def verify_id_token(id_token):
try:
# Fetch appropriate public key from provider JWKS
signing_key = jwk_client.get_signing_key_from_jwt(id_token)
# Verify signature, issuer (iss), and client audience (aud)
data = jwt.decode(
id_token,
signing_key.key,
algorithms=["RS256"],
audience="my-client-app-id",
issuer="https://identity-provider.com"
)
return data # Contains user profile info safely verified
except jwt.exceptions.InvalidTokenError as e:
print("Invalid token:", e)
return None
4. Token Storage Best Practices
Once tokens are acquired, storing them safely is paramount.
- Never store tokens in LocalStorage: LocalStorage is vulnerable to Cross-Site Scripting (XSS) attacks. If a third-party dependency is compromised, an attacker can steal the keys instantly.
- Use HttpOnly Cookies: For web apps with a backend session layer, store tokens in `HttpOnly; Secure; SameSite=Strict` cookies. Browsers prevent Javascript from reading `HttpOnly` cookies, fully mitigating XSS token theft.
- Token Refresh Rotation: Enforce refresh token rotation. Each time a client requests a new access token, issue a new refresh token and invalidate the old one. If an attacker intercepts a refresh token, reuse triggers a global revocation of the session.