OAuth 2.0 & OpenID Connect (OIDC)
Master delegated authorization and federated identity architecture: the 4 OAuth roles, Authorization Code Grant with PKCE, ID Tokens vs Access Tokens, Machine-to-Machine flows, and protection against authorization code interception.
1. The Anti-Pattern That Created OAuth: The Password Sharing Nightmare
In 2005, before OAuth existed, if a service like Yelp wanted to help you find your contacts on Gmail, Yelp showed you a prompt: “Enter your raw Gmail username and password”. Yelp saved your plaintext password in their database, logged in as you, and downloaded your contacts.
Why Direct Password Sharing Was a Catastrophe:
- Total Access: Yelp didn't just get access to your contacts — they could read all your emails, delete your calendar, and reset your bank passwords.
- No Revocation: To revoke Yelp's access, you had to change your master Gmail password, breaking every other app you used.
- Security Exposure: A single database breach at a tiny third-party app exposed master Google credentials for millions of people.
๐ The Valet Key Analogy
When you take your luxury car to a hotel valet, you don't give them your master house keys, your safe combination, or full administrative access to your vehicle.
Instead, you hand them a Valet Key: a specialized physical key that only unlocks the driver's door, starts the engine, locks the trunk, limits maximum speed to 25 MPH, and expires when you check out. That Valet Key is an OAuth 2.0 Scoped Access Token!
2. The 4 OAuth 2.0 Roles & AuthN vs. AuthZ
OAuth 2.0 formally defines four interacting entities in every authorization transaction:
The human being (e.g. Alice) who owns the data and can grant access to a third-party app.
The third-party web app, Single Page App (SPA), or mobile application requesting permission to access Alice's resources.
The trusted server (e.g. Google Accounts, Auth0, Okta, Keycloak) that authenticates the user and issues cryptographically signed tokens.
The backend API hosting the user's protected resources (e.g. Google Drive API, GitHub Repos API) that accepts and validates Access Tokens.
OAuth 2.0 = Authorization (AuthZ)
Question: “What is this application allowed to do?”
Artifact: Access Token (Opaque string or JWT). Meant for the Resource Server API. Contains scopes (e.g. photos.read, orders.write).
OpenID Connect (OIDC) = Authentication (AuthN)
Question: “Who is the user currently logged in?”
Artifact: ID Token (Always a signed JWT). Meant for the Client Application UI. Contains identity profile claims (sub, name, email, avatar).
3. Authorization Code Grant with PKCE (RFC 7636)
In public clients (React/Vue SPAs, iOS/Android apps), a client secret cannot be protected because client-side source code can be decompiled. PKCE (Proof Key for Code Exchange) solves this without requiring any shared secret:
If an attacker intercepts the authorization_code in step 4 via a custom mobile URI scheme or browser history leak, the attacker cannot exchange the code for tokens because they do not have the raw in-memory code_verifier string created by your client in step 1!
4. The 4 OAuth 2.0 Grant Types
Use Case: User-facing Web Apps, React/Vue SPAs, iOS/Android mobile apps.
Use Case: Machine-to-Machine background cron jobs or Microservice-to-Microservice communication (no human user present).
Use Case: Smart TVs, Apple TV, PlayStation, or CLI tools where typing a password on the device is impractical.
Use Case: Silently exchanging an expired 15-minute Access Token for a fresh one without prompting the user to re-login.
5. Interactive 7-Step PKCE Authorization Simulator
Step through the cryptographic handshakes of an OAuth 2.0 PKCE flow. Observe how the random code verifier, challenge hash, and authorization code dynamically validate in the browser.
6. PKCE Generation & Token Exchange in 5 Languages
Below is production-ready code demonstrating how to generate a cryptographic code_verifier, calculate the SHA-256 code_challenge, and execute the token exchange.
import os
import base64
import hashlib
import httpx
from typing import Tuple, Dict, Any
class PkceOAuthClient:
@staticmethod
def generate_pkce_pair() -> Tuple[str, str]:
# 1. Generate 32 bytes of cryptographically random entropy (Code Verifier)
raw_bytes = os.urandom(32)
code_verifier = base64.urlsafe_b64encode(raw_bytes).decode('utf-8').rstrip('=')
# 2. Compute SHA-256 hash (Code Challenge)
sha256_digest = hashlib.sha256(code_verifier.encode('utf-8')).digest()
code_challenge = base64.urlsafe_b64encode(sha256_digest).decode('utf-8').rstrip('=')
return code_verifier, code_challenge
@staticmethod
async def exchange_code_for_tokens(
auth_server_url: str,
client_id: str,
auth_code: str,
code_verifier: str,
redirect_uri: str
) -> Dict[str, Any]:
async with httpx.AsyncClient() as client:
res = await client.post(
f"{auth_server_url}/oauth/token",
data={
"grant_type": "authorization_code",
"client_id": client_id,
"code": auth_code,
"code_verifier": code_verifier,
"redirect_uri": redirect_uri
}
)
res.raise_for_status()
return res.json() # Returns { "access_token": "...", "id_token": "...", "expires_in": 3600 }
export class PkceOAuthClient {
static async generatePkcePair(): Promise<{ verifier: string; challenge: string }> {
// 1. Generate random 32-byte array
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const verifier = btoa(String.fromCharCode(...array))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// 2. Hash using Web Crypto API SHA-256
const encoder = new TextEncoder();
const digest = await crypto.subtle.digest('SHA-256', encoder.encode(verifier));
const challenge = btoa(String.fromCharCode(...new Uint8Array(digest)))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
return { verifier, challenge };
}
static async exchangeToken(
tokenEndpoint: string,
clientId: string,
authCode: string,
codeVerifier: string,
redirectUri: string
) {
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
code: authCode,
code_verifier: codeVerifier,
redirect_uri: redirectUri,
});
const res = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
});
return await res.json();
}
}
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
)
// GeneratePKCE creates a verifier and SHA256 challenge string
func GeneratePKCE() (verifier, challenge string, err error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", "", err
}
verifier = base64.RawURLEncoding.EncodeToString(bytes)
h := sha256.New()
h.Write([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(h.Sum(nil))
return verifier, challenge, nil
}
import java.security.SecureRandom;
import java.security.MessageDigest;
import java.util.Base64;
import java.nio.charset.StandardCharsets;
public class PkceUtil {
public static class PkcePair {
public final String verifier;
public final String challenge;
public PkcePair(String v, String c) { this.verifier = v; this.challenge = c; }
}
public static PkcePair generate() throws Exception {
byte[] randomBytes = new byte[32];
new SecureRandom().nextBytes(randomBytes);
String verifier = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(verifier.getBytes(StandardCharsets.US_ASCII));
String challenge = Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
return new PkcePair(verifier, challenge);
}
}
#include <iostream>
#include <string>
#include <vector>
#include <openssl/rand.h>
#include <openssl/sha.h>
// Demonstration of PKCE generation with OpenSSL
std::string base64UrlEncode(const unsigned char* input, size_t length);
struct PkcePair {
std::string verifier;
std::string challenge;
};
PkcePair generatePkce() {
unsigned char randomBytes[32];
RAND_bytes(randomBytes, sizeof(randomBytes));
std::string verifier = base64UrlEncode(randomBytes, sizeof(randomBytes));
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256(reinterpret_cast<const unsigned char*>(verifier.c_str()), verifier.length(), hash);
std::string challenge = base64UrlEncode(hash, sizeof(hash));
return { verifier, challenge };
}