edit_document// BLOG_POST.md

OAuth 2.0 and OpenID Connect: What Every Developer Gets Wrong

//

,

OAuth 2.0 is the most widely deployed authorization framework on the web. Google, GitHub, Microsoft, Apple, and virtually every SaaS platform use it to grant third-party applications limited access to user accounts. OpenID Connect (OIDC) is a thin identity layer built on top of OAuth that adds authentication: proving who a user is. Most developers conflate the two, implement them incorrectly, and create security vulnerabilities in the process. Here is what you need to understand.

OAuth 2.0 Is Authorization, Not Authentication

OAuth answers: “Can this application access this resource on behalf of this user?” It does not answer: “Who is this user?” An OAuth access token grants permission to call an API. It does not reliably identify the person who granted that permission. Using a plain OAuth access token as proof of identity is a common and dangerous mistake.

OIDC answers both questions. It extends OAuth with an id_token (a signed JWT containing the user’s identity claims: subject ID, email, name, authentication time) and a standardized /userinfo endpoint. If you need to know who someone is, use OIDC. If you only need to access their resources (calendar, files, repos), plain OAuth suffices.

The Authorization Code Flow (with PKCE)

This is the recommended flow for web applications, mobile apps, and SPAs. PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks and is now required for all public clients:

// Step 1: Generate PKCE verifier and challenge
function generatePKCE() {
  const verifier = crypto.randomUUID() + crypto.randomUUID();
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  return crypto.subtle.digest('SHA-256', data).then(hash => {
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
      .replace(/+/g, '-').replace(///g, '_').replace(/=+$/, '');
    return { verifier, challenge };
  });
}

// Step 2: Redirect user to authorization server
const { verifier, challenge } = await generatePKCE();
sessionStorage.setItem('pkce_verifier', verifier);

const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', CLIENT_ID);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', crypto.randomUUID()); // CSRF protection
window.location.href = authUrl.toString();
// Step 3: Exchange authorization code for tokens (server-side)
async function handleCallback(code: string): Promise<TokenResponse> {
  const verifier = session.get('pkce_verifier');

  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,  // Only for confidential clients
      code,
      redirect_uri: 'https://myapp.com/callback',
      code_verifier: verifier,
    }),
  });

  const tokens = await response.json();
  // tokens.access_token  -- for API calls
  // tokens.id_token      -- for user identity (OIDC)
  // tokens.refresh_token -- for obtaining new access tokens
  return tokens;
}

Validating the ID Token

The id_token is a JWT signed by the authorization server. You must validate it before trusting its claims:

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const client = jwksClient({ jwksUri: 'https://www.googleapis.com/oauth2/v3/certs' });

async function verifyIdToken(idToken: string): Promise<UserClaims> {
  const decoded = jwt.decode(idToken, { complete: true });
  if (!decoded?.header.kid) throw new Error('Missing kid in token header');

  const key = await client.getSigningKey(decoded.header.kid);
  const publicKey = key.getPublicKey();

  const payload = jwt.verify(idToken, publicKey, {
    algorithms: ['RS256'],
    audience: CLIENT_ID,         // Must match YOUR client ID
    issuer: 'https://accounts.google.com',
  }) as jwt.JwtPayload;

  return {
    sub: payload.sub!,           // Unique, stable user identifier
    email: payload.email,
    name: payload.name,
    emailVerified: payload.email_verified,
  };
}

Critical checks: verify the signature against the provider’s public keys (JWKS), confirm the aud (audience) matches your client ID, verify the iss (issuer) matches the expected provider, and check exp (expiration). Skipping any of these checks opens your application to token substitution attacks.

Common Mistakes

Using the access token as identity. Access tokens are opaque strings meant for API calls. They do not reliably identify users. Use the id_token for identity.

Not validating the state parameter. The state parameter prevents CSRF attacks. Generate a random value before the redirect, store it in the session, and verify it matches when the callback arrives.

Storing tokens in localStorage. LocalStorage is accessible to any JavaScript on the page, making it vulnerable to XSS. Use HTTP-only cookies for refresh tokens and keep access tokens in memory.

Not using PKCE. Even for server-side apps, PKCE adds defense in depth against authorization code interception. The OAuth 2.1 draft makes PKCE mandatory for all clients.

Trusting email as a unique identifier. Users can change their email. Use the sub claim as your primary identifier, which is guaranteed stable per provider.

When to Use a Library

Always. Do not implement OAuth/OIDC from scratch in production. Use battle-tested libraries: NextAuth.js (Next.js), Auth.js (framework-agnostic), Passport.js (Express), Laravel Socialite (PHP), or Spring Security OAuth (Java). These libraries handle token validation, session management, PKCE, and provider-specific quirks. Your job is to configure them correctly, not reimplement the protocol.

Further reading: OAuth 2.0 Specification | OpenID Connect | PKCE RFC 7636


arrow_circle_right// POST_NAVIGATION

forum// COMMENTS

Leave a Reply

Your email address will not be published. Required fields are marked *