Featured image of post OAuth 2.0 and OpenID Connect: Modern Authentication Guide

OAuth 2.0 and OpenID Connect: Modern Authentication Guide

Complete guide to OAuth 2.0 and OpenID Connect covering authorization flows, PKCE, ID tokens vs access tokens, refresh token rotation, and implementation with major providers.

OAuth 2.0 and OpenID Connect form the backbone of modern web authentication and authorization. Despite their ubiquity, these protocols are frequently misunderstood and misconfigured, leading to preventable security vulnerabilities. This guide covers the core concepts, implementation patterns, and security best practices you need to integrate authentication securely in your applications.

OAuth 2.0 Fundamentals

OAuth 2.0 is an authorization framework, not an authentication protocol. This distinction is critical: OAuth defines how a client application can obtain delegated access to protected resources, but it does not specify how to verify the user’s identity. That is where OpenID Connect comes in.

The protocol defines four roles:

RoleDescriptionExample
Resource OwnerEntity that can grant accessEnd user
ClientApplication requesting accessWeb app, mobile app
Authorization ServerIssues tokens after successful authenticationAuth0, Keycloak
Resource ServerHosts protected resourcesAPI server

The abstract flow is consistent across all grant types: the client requests authorization, the resource owner grants consent, the authorization server issues tokens, and the client uses those tokens to access resources.


Authorization Code Flow with PKCE

The authorization code flow with PKCE is the recommended grant for most applications, including native mobile apps and single-page applications. PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks by binding the authorization request to the token request through a cryptographic challenge.

// Step 1: Generate code verifier and challenge
function generatePKCE() {
  const verifier = crypto.randomBytes(32).toString("base64url");
  const challenge = crypto
    .createHash("sha256")
    .update(verifier)
    .digest("base64url");
  return { verifier, challenge };
}

// Step 2: Redirect user to authorization server
const { verifier, challenge } = generatePKCE();
const authUrl = new URL("https://authorization-server.com/authorize");
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("client_id", CLIENT_ID);
authUrl.searchParams.set("redirect_uri", CALLBACK_URL);
authUrl.searchParams.set("code_challenge", challenge);
authUrl.searchParams.set("code_challenge_method", "S256");
authUrl.searchParams.set("state", crypto.randomUUID());

// Step 3: Exchange authorization code for tokens
async function exchangeCode(code, verifier) {
  const response = await fetch("https://authorization-server.com/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: CALLBACK_URL,
      client_id: CLIENT_ID,
      code_verifier: verifier,
    }),
  });
  return response.json(); // { access_token, refresh_token, id_token, expires_in }
}

The state parameter provides CSRF protection. PKCE is now recommended even for server-side web applications, as it provides defense in depth against authorization code interception.


Grant Types Comparison

Grant TypeUse CaseSecurity Profile
Authorization Code + PKCEWeb, mobile, SPAStrongest — recommended for all new apps
Client CredentialsServer-to-server, service accountsNo user context, secure backend channel
Device AuthorizationCLI, IoT, smart TVsRequires user to visit a URL on another device
Authorization Code (no PKCE)Legacy server-side appsVulnerable to code interception
Implicit (deprecated)Legacy SPAsRemoved from spec — no longer secure

OpenID Connect Layer

OpenID Connect (OIDC) adds an identity layer on top of OAuth 2.0. The key addition is the id_token, a JWT that contains verified claims about the authenticated user.

// Verifying an ID token in Node.js
const jwksClient = require("jwks-rsa");
const jwt = require("jsonwebtoken");

const client = jwksClient({
  jwksUri: "https://authorization-server.com/.well-known/jwks.json",
});

async function verifyIdToken(idToken) {
  const decoded = jwt.decode(idToken, { complete: true });
  const key = await client.getSigningKey(decoded.header.kid);
  const signingKey = key.getPublicKey();

  return jwt.verify(idToken, signingKey, {
    issuer: "https://authorization-server.com",
    audience: CLIENT_ID,
    algorithms: ["RS256"],
  });
}

Standard ID token claims include sub (subject identifier), iss (issuer), aud (audience), exp (expiration), iat (issued at), and nonce (replay prevention). Always validate the signature, issuer, audience, and expiry before trusting an ID token.


ID Tokens vs. Access Tokens

A common source of confusion is the difference between ID tokens and access tokens. ID tokens are for authentication — they tell the client who the user is. Access tokens are for authorization — they tell the resource server what the client is allowed to do.

ID tokens must never be used for API authorization. Access tokens may be opaque (a random string that the resource server verifies via introspection) or JWTs (self-contained with embedded claims). Use the userinfo endpoint to retrieve additional claims about the user from the authorization server.


Refresh Token Rotation

Refresh token rotation is a security mechanism where each refresh operation returns a new refresh token and invalidates the previous one. This limits the window of opportunity if a refresh token is stolen.

async function refreshAccessToken(refreshToken) {
  const response = await fetch("https://authorization-server.com/token", {
    method: "POST",
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
    }),
  });
  const tokens = await response.json();
  // Old refresh token is invalidated; store the new one
  await secureStore.save("refresh_token", tokens.refresh_token);
  return tokens.access_token;
}

Major providers including Auth0, Okta, and Keycloak now enable refresh token rotation by default. Coupled with reuse detection (revoking all tokens if a rotated token is reused), this provides strong protection against token theft.


Security Best Practices

Token storage strategy is one of the most important security decisions. For browser-based applications, store access tokens in memory and use httpOnly cookies for refresh tokens. Never store tokens in localStorage, as it is accessible to any JavaScript on the same origin.

Storage MethodAccess TokenRefresh Token
In-memory variableBest for SPAs (not persisted)Not suitable
httpOnly cookieNot recommendedBest — protected from XSS
localStorageVulnerable to XSSVulnerable to XSS
Secure memory (WebAuthn)Emerging approachEmerging approach

Always validate redirect URIs server-side, use short-lived access tokens (15–60 minutes), and minimize requested scopes to the minimum required for functionality.


Conclusion

OAuth 2.0 and OpenID Connect are powerful but nuanced protocols. Use the authorization code flow with PKCE for all new applications, validate ID tokens thoroughly on the client side, implement refresh token rotation, and never store tokens in localStorage. By following these patterns and understanding the security properties of each grant type, you can build authentication systems that are both user-friendly and resistant to common attacks.