Table of Contents

    OAuth2 and OIDC

    SYSTEM DESIGN • CHAPTER 13.2

    OAuth 2.0 and OpenID Connect

    Understand how OAuth 2.0 delegates authorization between applications, how OpenID Connect adds verified user identity on top of it, and how to choose the right flow for each client type.

    Learning objective: By the end of this article, you will understand OAuth 2.0 roles, grants, tokens, and scopes, how OpenID Connect extends OAuth with authentication, and which flow and controls suit web apps, single-page apps, mobile apps, and service-to-service calls.

    Prerequisites

    Recommended Knowledge

    • HTTP methods, status codes, headers, and redirects
    • TLS and why transport security matters
    • Cookies, sessions, and browser storage
    • REST APIs and request authentication
    • Authentication versus authorization
    • JSON structure and Base64 URL encoding
    • Public-key cryptography and digital signatures
    • Trust boundaries from threat modeling

    The Problem Being Solved

    Suppose a reporting application needs to read a user's files from a storage platform. The naive approach is to ask the user for their storage password and log in on their behalf.

    Password Sharing Approach The application receives full account credentials, gains unlimited access, holds a secret it cannot protect properly, and cannot be revoked without changing the user's password.
    Delegated Authorization Approach The user authenticates directly with the storage platform, which then issues the application a limited, expiring credential restricted to specific permissions and revocable at any time.

    OAuth 2.0 is the framework that makes the second approach standard. It allows one application to act on a resource with limited permission, without ever seeing the user's credentials.

    Simple Analogy

    A hotel issues a key card that opens one room for a limited period, rather than handing over the master key. The card can be cancelled without changing every lock in the building.

    OAuth 2.0 Roles

    Role Responsibility Example
    Resource owner Owns the data and grants permission The end user
    Client Requests access on the owner's behalf The reporting application
    Authorization server Authenticates the owner and issues tokens The identity provider
    Resource server Hosts protected data and validates tokens The storage or business API
    BASIC OAUTH FLOW
    User Authorization Server Client Resource Server

    OAuth 2.0 Is Not Authentication

    This is the most consequential misunderstanding in the topic. OAuth 2.0 is an authorization framework. An access token states that a client may perform certain operations. It does not, by itself, reliably tell the client who the user is.

    Unsafe Assumption

    • Treating an access token as proof of identity
    • Calling a profile endpoint and trusting the result blindly
    • Assuming the token was issued for this specific client
    • Logging users in based on an opaque access token

    Correct Approach

    • Use OpenID Connect for authentication
    • Validate an ID token's signature and claims
    • Verify the audience matches this client
    • Keep identity and API authorization distinct
    OAuth 2.0 answers what a client may do. OpenID Connect answers who the user is. Production systems usually need both.

    What OpenID Connect Adds

    OpenID Connect, commonly written as OIDC, is an identity layer built on top of OAuth 2.0. It reuses the same flows and endpoints but adds a standardized way to prove and describe user identity.

    1

    The ID Token

    A signed JSON Web Token containing claims about the authentication event and the user. Unlike an access token, it is intended for the client to read and validate.

    2

    Standard Claims

    A defined vocabulary for identity attributes such as subject, issuer, audience, name, and email, so clients are not dependent on provider-specific formats.

    3

    The UserInfo Endpoint

    A protected endpoint returning additional profile claims, accessed using the issued access token.

    4

    Discovery and Key Publication

    A well-known configuration document and a published key set allowing clients to locate endpoints and obtain signing keys programmatically.

    Aspect OAuth 2.0 OpenID Connect
    Primary purpose Delegated authorization Authentication and identity
    Core question What may this client access? Who is this user?
    Main token Access token ID token
    Token audience The resource server The client application
    Token format Opaque or JWT, provider dependent JWT with defined claims
    Triggering scope API-specific scopes The openid scope

    The Three Token Types

    Token Purpose Intended Consumer Typical Lifetime
    Access token Authorizes API calls Resource server Short
    ID token Proves an authentication occurred Client application Short
    Refresh token Obtains new access tokens Authorization server Longer
    TOKEN RULE
    Each token has one intended audience. A client should never send an ID token to an API, nor attempt to interpret an opaque access token.

    Example ID Token Claims

    {
        "iss": "https://identity.example.com",
        "sub": "user-4821",
        "aud": "reporting-web-client",
        "exp": 1790000000,
        "iat": 1789996400,
        "auth_time": 1789996390,
        "nonce": "n-3f9a2c7b",
        "email": "user@example.com",
        "email_verified": true,
        "name": "Example User"
    }
    Claim Meaning Why It Must Be Checked
    iss Issuing authorization server Rejects tokens from an unexpected issuer
    sub Stable user identifier Safer account key than a changeable email
    aud Intended client Prevents accepting a token issued for another app
    exp Expiry time Prevents indefinite token reuse
    iat Issue time Supports freshness evaluation
    nonce Value bound to the original request Detects token replay into a different session
    Account linking caution: Prefer the issuer and subject pair as the account key. An email address can change, and an unverified email should never be trusted to link an existing account.

    The Authorization Code Flow

    The authorization code flow is the recommended default for applications acting on behalf of a user. The browser never carries tokens in its address bar, because a short-lived code is exchanged for tokens over a direct back-channel request.

    AUTHORIZATION CODE FLOW
    Redirect to Login User Consents Code Returned Code Exchanged for Tokens
    1. The client redirects the user to the authorization server.
    2. The user authenticates and approves the requested scopes.
    3. The server redirects back with a single-use authorization code.
    4. The client verifies the returned state value.
    5. The client exchanges the code at the token endpoint.
    6. The server returns access, ID, and possibly refresh tokens.
    7. The client validates the ID token before establishing a session.
    8. The client calls APIs using the access token.

    Authorization Request

    GET /authorize
        ?response_type=code
        &client_id=reporting-web-client
        &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
        &scope=openid%20profile%20email%20reports.read
        &state=stt-7c1d9e4a
        &nonce=n-3f9a2c7b
        &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
        &code_challenge_method=S256 HTTP/1.1
    Host: identity.example.com

    Token Exchange

    {
        "grant_type": "authorization_code",
        "code": "auth-code-9f2b71c4",
        "redirect_uri": "https://app.example.com/callback",
        "client_id": "reporting-web-client",
        "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
    }

    Token Response

    {
        "access_token": "opaque-or-jwt-access-token",
        "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
        "refresh_token": "refresh-token-value",
        "token_type": "Bearer",
        "expires_in": 900,
        "scope": "openid profile email reports.read"
    }

    PKCE: Proof Key for Code Exchange

    PKCE protects the authorization code against interception. The client generates a random secret before starting the flow, sends only its hash in the authorization request, and reveals the original value during the token exchange.

    PKCE BINDING
    \[ challenge = \text{BASE64URL}(\text{SHA-256}(verifier)) \]

    An attacker who steals the authorization code cannot exchange it, because they do not possess the verifier that matches the challenge recorded at the authorization server.

    function base64UrlEncode(buffer) {
        return btoa(String.fromCharCode(...new Uint8Array(buffer)))
            .replace(/\+/g, "-")
            .replace(/\//g, "_")
            .replace(/=+$/, "");
    }
    
    async function createPkcePair() {
        const randomBytes = new Uint8Array(32);
        crypto.getRandomValues(randomBytes);
    
        const codeVerifier = base64UrlEncode(randomBytes.buffer);
    
        const digest = await crypto.subtle.digest(
            "SHA-256",
            new TextEncoder().encode(codeVerifier)
        );
    
        return {
            codeVerifier: codeVerifier,
            codeChallenge: base64UrlEncode(digest),
            codeChallengeMethod: "S256"
        };
    }
    Current guidance: PKCE is no longer considered a mobile-only measure. It is recommended for all clients using the authorization code flow, including confidential server-side applications.

    State and Nonce

    These two parameters defend against different attacks and are frequently confused.

    State

    • Random value tied to the browser session
    • Returned unchanged on the redirect
    • Defends against cross-site request forgery
    • Verified before the code is exchanged

    Nonce

    • Random value sent in the authentication request
    • Embedded as a claim inside the ID token
    • Defends against ID token replay
    • Verified during ID token validation

    Choosing the Right Grant

    Grant Use Case Status
    Authorization code with PKCE Web apps, single-page apps, mobile apps Recommended default
    Client credentials Service-to-service calls with no user present Recommended for machine identity
    Device authorization Televisions, consoles, and input-limited devices Recommended for that context
    Refresh token Renewing access without re-prompting the user Recommended with rotation
    Implicit Historically used by browser applications Discouraged in current guidance
    Resource owner password Client collects the user's password directly Discouraged in current guidance
    Why Implicit Was Deprecated It returned access tokens directly in the redirect fragment, exposing them to browser history, referrer leakage, and scripts running in the page, with no equivalent of the PKCE binding.

    Client Credentials for Service-to-Service

    When no user is involved, the client authenticates as itself. A background indexing job calling an internal API is a typical example.

    {
        "grant_type": "client_credentials",
        "client_id": "search-indexer",
        "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
        "client_assertion": "eyJhbGciOiJSUzI1NiJ9...",
        "scope": "catalog.read index.write"
    }

    Machine Identity Practices

    • Prefer signed client assertions over shared secrets
    • Issue a distinct identity per workload
    • Grant only the scopes that workload requires
    • Rotate credentials and keys on a defined schedule
    • Store secrets in a managed secret store, never in source
    • Log and alert on unexpected scope usage

    Validating Tokens Correctly

    Validation is where most implementation defects appear. Every check below exists because skipping it enables a specific attack.

    1

    Verify the Signature

    Retrieve the issuer's published keys, select the key matching the token's key identifier, and verify the signature. Cache keys, and refresh on rotation.

    2

    Enforce the Expected Algorithm

    Accept only algorithms your configuration permits. Never let the token's own header dictate the verification method, and never accept an unsigned token.

    3

    Check Issuer and Audience

    Confirm the issuer matches the configured authorization server and the audience matches this application or API.

    4

    Check Time Claims

    Reject expired tokens and tokens not yet valid, allowing only a small tolerance for clock differences.

    5

    Check Nonce and Scopes

    Confirm the ID token nonce matches the value stored for this login attempt, and confirm the access token carries the scopes the endpoint requires.

    async function validateIdToken(idToken, expected) {
        const { header, payload, signature } = decodeJwt(idToken);
    
        if (!expected.allowedAlgorithms.includes(header.alg)) {
            throw new Error("Unsupported signing algorithm");
        }
    
        const signingKey = await getSigningKey(
            expected.issuer,
            header.kid
        );
    
        const signatureValid = await verifySignature(
            idToken,
            signingKey,
            header.alg
        );
    
        if (!signatureValid) {
            throw new Error("Invalid token signature");
        }
    
        if (payload.iss !== expected.issuer) {
            throw new Error("Unexpected issuer");
        }
    
        const audiences = Array.isArray(payload.aud)
            ? payload.aud
            : [payload.aud];
    
        if (!audiences.includes(expected.clientId)) {
            throw new Error("Token audience mismatch");
        }
    
        const nowSeconds = Math.floor(Date.now() / 1000);
    
        if (payload.exp <= nowSeconds - expected.clockSkewSeconds) {
            throw new Error("Token expired");
        }
    
        if (payload.nonce !== expected.nonce) {
            throw new Error("Nonce mismatch");
        }
    
        return payload;
    }
    Dangerous Shortcut Decoding a token and reading its claims without verifying the signature. Anyone can craft a token body, so unverified claims carry no security value whatsoever.

    Opaque Tokens and Introspection

    Not all access tokens are JWTs. An opaque token is a reference that carries no readable content, and the resource server asks the authorization server about its validity.

    Property JWT Access Token Opaque Access Token
    Validation Local signature verification Introspection call to the issuer
    Latency Minimal per request Network call unless cached
    Revocation Effective only at expiry unless checked Effective immediately
    Content exposure Claims readable by anyone holding it No embedded information
    Availability coupling Independent of the issuer at request time Depends on issuer availability
    REVOCATION TRADE-OFF
    A self-contained token remains valid until it expires. Short lifetimes are the practical compensation for the absence of instant revocation.

    Refresh Tokens and Rotation

    Refresh tokens let a client obtain new access tokens without interrupting the user. Because they are long-lived, they are a high-value target and require stronger handling.

    ROTATION WITH REUSE DETECTION
    Refresh Used New Token Issued Old Token Invalidated Reuse Revokes Family
    CREATE TABLE refresh_tokens (
        token_id        VARCHAR(64) PRIMARY KEY,
        family_id       VARCHAR(64) NOT NULL,
        subject_id      VARCHAR(64) NOT NULL,
        client_id       VARCHAR(64) NOT NULL,
        token_hash      VARCHAR(128) NOT NULL,
        issued_at       TIMESTAMP NOT NULL,
        expires_at      TIMESTAMP NOT NULL,
        consumed_at     TIMESTAMP NULL,
        revoked_at      TIMESTAMP NULL
    );
    
    UPDATE refresh_tokens
    SET revoked_at = CURRENT_TIMESTAMP
    WHERE family_id = :family_id
      AND revoked_at IS NULL;

    If a previously consumed refresh token is presented again, the authorization server should assume theft and revoke the entire token family, forcing a fresh authentication.

    Scopes Versus Permissions

    A scope describes what the client was authorized to request. It does not describe what the user is entitled to access. Both checks are required.

    EFFECTIVE ACCESS
    \[ Access = Scope \cap UserPermission \]
    Insufficient Check The token includes reports.read, so the API returns the requested report without verifying that this particular user may view that particular report.
    Complete Check The API confirms the scope permits report reading, then separately confirms that the authenticated subject is authorized for the specific resource being requested.
    async function getReport(request, response) {
        const claims = request.tokenClaims;
    
        if (!claims.scope.split(" ").includes("reports.read")) {
            return response.status(403).json({
                error: "insufficient_scope"
            });
        }
    
        const report = await reportRepository.findById(
            request.params.reportId
        );
    
        if (!report) {
            return response.status(404).json({ error: "not_found" });
        }
    
        const permitted = await authorizationService.canView(
            claims.sub,
            report
        );
    
        if (!permitted) {
            return response.status(404).json({ error: "not_found" });
        }
    
        return response.json(report);
    }
    Enumeration note: Returning a not-found response for unauthorized resources avoids confirming that a given identifier exists.

    Threats and Mitigations

    Threat Description Mitigation
    Open redirect Redirect URI is manipulated to an attacker site Exact-match registered redirect URIs
    Code interception Authorization code is captured in transit PKCE with the S256 method
    CSRF on callback Attacker initiates a flow in the victim's browser State parameter bound to the session
    ID token replay A captured token is reused in another session Nonce validation and short lifetimes
    Token substitution A token issued for another client is presented Strict audience validation
    Algorithm confusion Token header dictates weaker verification Allow-list algorithms server-side
    Token theft via scripting Malicious script reads browser-stored tokens HttpOnly cookies and content security policy
    Refresh token theft Stolen refresh token grants prolonged access Rotation with reuse detection
    Excessive scope Client requests more access than needed Granular scopes and consent review
    Consent phishing A malicious app requests broad permissions App verification and admin consent policies

    Where Browser Applications Should Store Tokens

    Local Storage

    • Readable by any script on the page
    • Exposed by a single scripting vulnerability
    • Exposed by a compromised third-party library
    • Persists beyond the browsing session

    Backend-Managed Session

    • Tokens held server-side, not in the browser
    • Session cookie marked HttpOnly and Secure
    • SameSite attribute limits cross-site sending
    • Server can revoke the session immediately

    The backend-for-frontend pattern places a server component between the browser and the APIs. The browser holds only a session cookie, while the server holds the tokens and attaches them to outbound API calls.

    Logout and Session Termination

    Clearing a local session is not a complete logout. Several layers may retain valid state.

    Complete Logout Requires

    • Clearing the application session and cookie
    • Revoking the refresh token at the authorization server
    • Ending the session at the identity provider
    • Notifying other applications sharing the session
    • Accepting that issued access tokens remain valid until expiry
    • Recording the logout event in the audit trail

    Discovery Configuration

    {
        "issuer": "https://identity.example.com",
        "authorization_endpoint": "https://identity.example.com/authorize",
        "token_endpoint": "https://identity.example.com/token",
        "userinfo_endpoint": "https://identity.example.com/userinfo",
        "jwks_uri": "https://identity.example.com/.well-known/jwks.json",
        "end_session_endpoint": "https://identity.example.com/logout",
        "response_types_supported": ["code"],
        "id_token_signing_alg_values_supported": ["RS256", "ES256"],
        "code_challenge_methods_supported": ["S256"]
    }

    Using discovery allows clients to adapt to endpoint and key changes without redeployment, provided the document is retrieved over a verified connection and cached sensibly.

    Monitoring and Detection

    Signals Worth Tracking

    • Failed token validations by reason
    • Audience and issuer mismatch attempts
    • Refresh token reuse detections
    • Authorization failures by client and endpoint
    • Unusual scope combinations being requested
    • Token requests from unexpected regions
    • Spikes in consent grants for a new client
    • Key rotation and signing key fetch failures
    • Latency and error rate of the introspection endpoint

    Common Implementation Mistakes

    Weak Implementation

    • Using OAuth alone as a login mechanism
    • Reading claims without verifying signatures
    • Skipping audience validation
    • Omitting state or nonce
    • Allowing wildcard redirect URIs
    • Storing tokens in browser local storage
    • Issuing long-lived access tokens
    • Treating scope as complete authorization
    • Linking accounts by unverified email

    Strong Implementation

    • Uses OIDC for identity and OAuth for API access
    • Fully validates signature and claims
    • Enforces exact redirect URI matching
    • Applies PKCE on every code flow
    • Keeps tokens out of the browser where possible
    • Issues short access tokens with rotating refresh
    • Checks scope and resource permission separately
    • Keys accounts on issuer and subject
    • Monitors and alerts on validation failures

    System Design Interview Discussion

    Question What Your Answer Should Cover
    Which flow and why? Client type, credential confidentiality, and PKCE
    How is the user identified? ID token validation and the subject claim
    Where do tokens live? Server-side session versus browser storage trade-offs
    How are tokens validated? Signature, issuer, audience, expiry, nonce, and scope
    How is access revoked? Refresh revocation, short lifetimes, and introspection
    How do services authenticate? Client credentials with per-workload identity
    What if the identity provider is unavailable? Key caching, graceful degradation, and retry behavior
    How is misuse detected? Reuse detection, anomaly monitoring, and audit logs

    Implementation Checklist

    Production Checklist

    • Use the authorization code flow with PKCE
    • Register exact redirect URIs without wildcards
    • Send and verify state on every request
    • Send and verify nonce for authentication flows
    • Verify token signatures against published keys
    • Allow-list acceptable signing algorithms
    • Validate issuer, audience, and expiry
    • Key user accounts on issuer and subject
    • Request the minimum necessary scopes
    • Enforce resource-level authorization independently
    • Keep access token lifetimes short
    • Rotate refresh tokens and detect reuse
    • Store tokens server-side where feasible
    • Support revocation and end-session endpoints
    • Cache signing keys and handle rotation gracefully
    • Enforce TLS on every endpoint in the flow
    • Audit consent grants and authorization failures
    • Test expired, tampered, and mismatched-audience tokens

    Knowledge Check

    1

    What problem does OAuth 2.0 solve?

    It allows an application to access resources on a user's behalf with limited, revocable permission, without receiving the user's credentials.

    2

    How does OIDC differ from OAuth 2.0?

    OIDC adds an authentication layer, introducing a signed ID token with standard identity claims intended for the client.

    3

    What does PKCE protect against?

    Interception of the authorization code, by requiring a secret verifier that only the legitimate client possesses.

    4

    Why validate the audience claim?

    Without it, a token legitimately issued for a different application could be accepted, allowing token substitution.

    5

    Why is scope insufficient for authorization?

    Scope describes what the client may attempt. The API must still verify that the specific user is entitled to the specific resource.

    Summary

    OAuth 2.0 is a delegated authorization framework built around four roles and several grant types. It replaces credential sharing with limited, expiring, revocable tokens issued by an authorization server.

    OpenID Connect layers authentication on top of OAuth 2.0, introducing the ID token with standardized claims, a UserInfo endpoint, and discovery metadata. Access tokens authorize API calls, ID tokens establish identity, and refresh tokens renew access.

    The authorization code flow with PKCE is the recommended default for user-facing clients, while client credentials suits service-to-service communication. Implicit and password grants are discouraged in current guidance.

    Correctness depends on disciplined validation: verify the signature, enforce the expected algorithm, check issuer, audience, expiry, and nonce, and then enforce resource-level authorization separately from scope.

    Key Takeaway

    OAuth 2.0 grants access; OpenID Connect proves identity. Use the authorization code flow with PKCE, validate every token claim rather than trusting its contents, keep access tokens short-lived with rotating refresh tokens, and treat scope as a ceiling rather than as a complete authorization decision.