Skip to main content
LadeStack LogoLadeStack
Security in Web Apps 7 min read November 24, 2024

Implementing OAuth 2.0 and OpenID Connect: Modern Architecture and Security Best Practices

Engineering guide to modern identity and authorization: OAuth 2.0 vs OIDC, Authorization Code with PKCE, JWT validation, token rotation, and BFF architectures.

OAuth 2.0 and OpenID Connect Architecture Diagram

1. Authentication vs. Authorization: Untangling the Core Confusion

A persistent source of architectural vulnerability in modern application engineering is the conflation of authentication (AuthN) and authorization (AuthZ).

  • OAuth 2.0 (RFC 6749) is strictly an authorization framework. It specifies protocols for a resource owner to delegate access to their protected resources to a third-party application without sharing their credentials. OAuth 2.0 issues Access Tokens. OAuth 2.0 has no concept of who the user is, how they authenticated, or when they logged in.
  • OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. It standardizes authentication by introducing an ID Token (a signed JSON Web Token describing the authenticated subject) and a standardized userinfo endpoint.

Using an OAuth 2.0 access token as proof of authentication—or trusting user identity simply because a client received a bearer token—creates serious authentication bypass vulnerabilities.

+--------------------------------------------------------------------------+
|                          Identity & Access Topology                      |
+------------------------------------+-------------------------------------+
                                     |
               +---------------------+---------------------+
               |                                           |
               v                                           v
    OAuth 2.0 (Authorization)                   OpenID Connect (Authentication)
    - Protocol: RFC 6749 / RFC 8414             - Specification: OIDC Core 1.0
    - Artifact: Access Token                    - Artifact: ID Token (JWT)
    - Audience: Resource Server (API)           - Audience: Client Application (RP)
    - Question: "What is client allowed to do?" - Question: "Who is the user?"

2. The Four OAuth 2.0 Roles

  1. Resource Owner: The entity capable of granting access to a protected resource (typically the human end-user).
  2. Client: The application requesting access to protected resources on behalf of the resource owner (Single Page App, mobile app, or backend microservice).
  3. Authorization Server: The system issuing tokens to clients after successfully authenticating the resource owner and obtaining consent (e.g., Auth0, Okta, Keycloak, AWS Cognito).
  4. Resource Server: The HTTP server hosting protected resources, capable of accepting and validating incoming access tokens (your backend API gateway or microservice).

3. The Standard: Authorization Code Flow with PKCE

Under modern security specifications (including the OAuth 2.0 Security Best Current Practice RFC 9700 and the OAuth 2.1 specification draft), the legacy Implicit Grant is officially deprecated and forbidden.

The gold standard for both confidential clients (server-side web apps) and public clients (Single Page Apps and mobile native apps) is the Authorization Code Flow with Proof Key for Code Exchange (PKCE, RFC 7636).

Client App (Browser)           Authorization Server           Backend API (Resource Server)
     |                                   |                                   |
     | 1. Generate code_verifier         |                                   |
     |    Calculate code_challenge (S256)|                                   |
     | 2. GET /authorize with challenge  |                                   |
     |---------------------------------->|                                   |
     |                                   |                                   |
     | 3. User authenticates & consents  |                                   |
     | 4. Redirect with auth_code        |                                   |
     |<----------------------------------|                                   |
     |                                   |                                   |
     | 5. POST /token + code_verifier    |                                   |
     |---------------------------------->|                                   |
     | 6. Validate verifier vs challenge |                                   |
     |    Issue ID & Access Tokens       |                                   |
     |<----------------------------------|                                   |
     |                                                                       |
     | 7. Request protected data (Bearer Access Token)                       |
     |---------------------------------------------------------------------->|
     | 8. Verify token signature via JWKS & process request                  |
     |<----------------------------------------------------------------------|

The Cryptographic Mechanics of PKCE

  1. The client generates a high-entropy cryptographically random string: code_verifier (between 43 and 128 characters, unreserved URL characters).
  2. The client calculates the SHA-256 hash and Base64URL-encodes it: $$\text{code_challenge} = \text{BASE64URL-ENCODE}(\text{SHA256}(\text{code_verifier}))$$
  3. The authorization request includes code_challenge and code_challenge_method=S256, along with a random state parameter for CSRF mitigation and a nonce parameter for replay protection.
  4. When exchanging the temporary authorization code, the client sends the raw code_verifier. Even if an attacker intercepts the authorization code via redirect hijacking, they cannot redeem it without the original code_verifier.

4. Secure Validation of ID Tokens and Access Tokens

When a Resource Server receives an incoming HTTP request containing an Authorization: Bearer <token> header, it must perform rigorous cryptographic and semantic validation before processing data.

Validation Sequence

import { jwtVerify, createRemoteJWKSet } from 'jose';

// Fetch public signing keys from identity provider's JWKS endpoint
const JWKS = createRemoteJWKSet(
  new URL('https://auth.ladestack.in/.well-known/jwks.json')
);

export async function validateAccessToken(bearerHeader: string | undefined) {
  if (!bearerHeader?.startsWith('Bearer ')) {
    throw new Error('Missing or invalid Authorization header');
  }

  const token = bearerHeader.split(' ')[1];

  // Cryptographically verify signature and standard claims
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: 'https://auth.ladestack.in/',
    audience: 'https://api.ladestack.in',
    algorithms: ['RS256', 'ES256'], // Explicitly whitelist asymmetric algorithms; forbid 'none' or 'HS256'
  });

  // Verify token expiry and active scope
  const scopes = (payload.scope as string)?.split(' ') || [];
  if (!scopes.includes('read:telemetry')) {
    throw new Error('Insufficient scope permissions');
  }

  return {
    subject: payload.sub,
    tenantId: payload['https://ladestack.in/tenant_id'],
    scopes,
  };
}

Essential JWT Security Rules

  • Reject alg: "none": Ensure your JWT library disallows unsigned tokens.
  • Algorithm Confusion Attacks: When using asymmetric signing (RS256), ensure the verification library does not accept symmetric verification (HS256) where an attacker signs a forgery using the server’s public key as an HMAC secret.
  • Audience & Issuer Validation: Always validate that aud matches your specific API identifier and iss matches your identity provider domain.

5. Token Storage Architecture: The Backend-for-Frontend (BFF) Pattern

Where should tokens live in single-page browser applications?

The Vulnerability of Browser Storage

Storing access tokens and refresh tokens in localStorage or sessionStorage exposes them directly to Cross-Site Scripting (XSS). Any compromised third-party script, malicious NPM dependency, or CDN injection can read storage and exfiltrate credentials instantly.

The BFF Architecture

To eliminate token exposure in the browser, modern enterprise architectures implement the Backend-for-Frontend (BFF) pattern:

Browser (SPA)                BFF Proxy Node / Gateway                IdP / Microservices
     |                                   |                                   |
     | GET /api/data                     |                                   |
     | Cookie: __Host-SessionId=xxx      |                                   |
     |---------------------------------->| 1. Read encrypted server session  |
     |                                   | 2. Extract raw Access Token       |
     |                                   | 3. Forward request with Bearer    |
     |                                   |---------------------------------->|
     |                                   |<----------------------------------|
     |<----------------------------------|                                   |
  1. The browser application never touches raw OAuth tokens.
  2. The authorization exchange occurs securely between the BFF and the Authorization Server.
  3. The BFF establishes a secure HTTP-only session with the client using cookies flagged with HttpOnly; Secure; SameSite=Strict; Path=/.
  4. The BFF attaches the raw bearer token when proxying calls to downstream APIs.

6. Refresh Token Rotation and Revocation

Refresh tokens grant long-lived access. If stolen, attackers maintain persistent access.

Refresh Token Rotation (RTR) mandates that whenever a refresh token is exchanged for a new access token, the Authorization Server invalidates the old refresh token and issues a single new one.

Client                              Authorization Server
  |                                          |
  | POST /token (RefreshToken_A)             |
  |----------------------------------------->|
  |                                          | Invalidate RefreshToken_A
  |                                          | Issue AccessToken_2 + RefreshToken_B
  |<-----------------------------------------|
  |                                          |
  | (Attacker attempts reuse of Token_A)     |
  | POST /token (RefreshToken_A)             |
  |----------------------------------------->|
  |                                          | ALARM: Detected Reuse!
  |                                          | Instantly revoke Token_B & invalidate
  |                                          | entire user session family.
  |<-----------------------------------------|

If an invalidated refresh token is ever submitted, the server detects token theft and immediately revokes all tokens within that session family, forcing a full re-authentication.


Production OAuth 2.0 / OIDC Verification Checklist

  • Authorization Code Flow with PKCE (S256) enforced for all clients.
  • Implicit grant and Resource Owner Password Credentials grant disabled.
  • Redirect URIs enforced via exact string matching; no wildcard domains.
  • High-entropy state parameter validated to prevent CSRF during authorization.
  • nonce verified in ID Token to prevent token replay attacks.
  • Access tokens validated against remote JWKS with strict iss, aud, and exp checks.
  • Tokens stored in HttpOnly; Secure; SameSite=Strict cookies via BFF architecture.
  • Refresh Token Rotation enabled with automatic reuse detection and revocation.
  • Standard RP-initiated logout clears both local application sessions and IdP SSO cookies.

More Engineering Articles