SystemDesign.website
Track 8 ยท Module 8.2Security & Authentication

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.

โฑ๏ธ Estimated Reading: 30 minsโ€ขPrerequisites: Module 8.1 (JWTs)โ€ขInteractive Lab: 7-Step PKCE Flow Lab
๐Ÿ”‘ REAL-WORLD ANALOGY ยท VALET KEY

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!

๐Ÿ›๏ธ RFC 6749 SPECIFICATION

2. The 4 OAuth 2.0 Roles & AuthN vs. AuthZ

OAuth 2.0 formally defines four interacting entities in every authorization transaction:

1. Resource Owner (User)

The human being (e.g. Alice) who owns the data and can grant access to a third-party app.

2. Client (Application)

The third-party web app, Single Page App (SPA), or mobile application requesting permission to access Alice's resources.

3. Authorization Server (IdP)

The trusted server (e.g. Google Accounts, Auth0, Okta, Keycloak) that authenticates the user and issues cryptographically signed tokens.

4. Resource Server (API)

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).

๐Ÿ›ก๏ธ THE MODERN GOLD STANDARD

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:

Full 7-Step OAuth 2.0 + PKCE Protocol Flow
SPA / AppUser BrowserAuthorization ServerAPI Server1. Generate PKCE2. GET /authorize?response_type=code&code_challenge=SHA256(verifier)&state=xyz3. Prompt Login & Consent Screen4. 302 Redirect to /callback?code=AUTH_CODE_123&state=xyz5. POST /oauth/token (code=AUTH_CODE_123 & code_verifier=RAW_STRING)6. Verify SHA256(verifier) == challenge → Issue ID Token + Access Token7. GET /api/v1/user/contacts (Header: "Authorization: Bearer ACCESS_TOKEN")
๐Ÿ’ก Why PKCE Makes Code Interception Impossible:

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!

๐Ÿ“ GRANT TYPE TAXONOMY

4. The 4 OAuth 2.0 Grant Types

1. Authorization Code + PKCE

Use Case: User-facing Web Apps, React/Vue SPAs, iOS/Android mobile apps.

2. Client Credentials Grant (M2M)

Use Case: Machine-to-Machine background cron jobs or Microservice-to-Microservice communication (no human user present).

3. Device Authorization Grant (RFC 8628)

Use Case: Smart TVs, Apple TV, PlayStation, or CLI tools where typing a password on the device is impractical.

4. Refresh Token Grant

Use Case: Silently exchanging an expired 15-minute Access Token for a fresh one without prompting the user to re-login.

๐Ÿงช LIVE PKCE FLOW LAB

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.

CURRENT STAGE: Step 1: Generate PKCE Secrets
๐Ÿ’ป PRODUCTION PATTERNS ยท CODE TEMPLATES

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.

pkce_oauth_client
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 };
}
        
๐ŸŽฏ ARCHITECTURE INTERVIEWS

7. OAuth 2.0 & OIDC Scenario Quizzes

Scenario 1: You are designing the authentication architecture for a nightly billing microservice that pulls payment records from an internal orders database. There is no human user present. Which OAuth grant type must you use?

Scenario 2: Why was the legacy OAuth 2.0 “Implicit Grant” officially deprecated in favor of Authorization Code with PKCE?

โ—‹

Completed this module?

Mark as finished to update your curriculum progress tracker.