Home/Blog/OAuth 2.0 & OpenID Connect Implementation Guide
Development

OAuth 2.0 & OpenID Connect Implementation Guide

Complete guide to implementing OAuth 2.0 and OpenID Connect (OIDC) for API authentication. Covers Authorization Code with PKCE, Client Credentials, token management, and security best practices aligned with OAuth 2.1.

OAuth 2.0 & OpenID Connect Implementation Guide

OAuth 2.0 and OpenID Connect (OIDC) are the industry standards for API authentication and authorization. This guide covers everything you need to implement secure OAuth/OIDC flows, aligned with the latest OAuth 2.1 security best practices.

OAuth 2.0 vs OpenID Connect

┌─────────────────────────────────────────────────────────────────┐
│                    OAUTH 2.0 vs OIDC                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  OAuth 2.0 = AUTHORIZATION                                      │
│  "What can this app access?"                                    │
│  └─► Access Tokens → API Access                                 │
│                                                                 │
│  OpenID Connect = AUTHENTICATION + Authorization                │
│  "Who is this user?" + "What can they access?"                  │
│  └─► ID Tokens → User Identity                                  │
│  └─► Access Tokens → API Access                                 │
│                                                                 │
│  OIDC = OAuth 2.0 + Identity Layer                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Grant Type Selection Guide

What type of client are you building?
│
├─► User-facing app (web, mobile, SPA)
│   │
│   └─► Use: Authorization Code + PKCE
│       • User authenticates at IdP
│       • Code exchanged for tokens
│       • Works for all client types
│
├─► Server-to-server (no user)
│   │
│   └─► Use: Client Credentials
│       • Machine-to-machine auth
│       • Client authenticates directly
│       • No user involvement
│
├─► Device with no keyboard (TV, IoT)
│   │
│   └─► Use: Device Authorization Grant
│       • User authenticates on phone/computer
│       • Device polls for tokens
│       • Good for CLI tools too
│
└─► ❌ DEPRECATED (Don't use)
    • Implicit Grant - tokens exposed in URL
    • ROPC - user credentials shared with app

Authorization Code Flow with PKCE

This is the recommended flow for all user-facing applications.

Flow Diagram

┌─────────┐                                    ┌─────────┐
│  User   │                                    │   IdP   │
└────┬────┘                                    └────┬────┘
     │                                              │
     │  1. Click "Login"                            │
     ▼                                              │
┌─────────┐  2. Redirect with code_challenge  ┌────┴────┐
│  App    │ ─────────────────────────────────►│  Auth   │
│ (Client)│                                   │Endpoint │
└────┬────┘                                   └────┬────┘
     │                                              │
     │         3. User authenticates                │
     │         4. User consents to scopes           │
     │                                              │
     │  5. Redirect with authorization code         │
     │◄─────────────────────────────────────────────│
     │                                              │
     │  6. Exchange code + code_verifier       ┌────┴────┐
     │ ─────────────────────────────────────────►│ Token  │
     │                                         │Endpoint │
     │  7. Receive tokens                      └────┬────┘
     │◄─────────────────────────────────────────────│
     │                                              │
     │  8. Use access_token for API calls           │
     ▼                                              ▼
┌─────────┐                                    ┌─────────┐
│   API   │◄───────────────────────────────────│ Tokens  │
└─────────┘                                    └─────────┘

Step 1: Generate PKCE Challenge

// Generate cryptographically random code verifier
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64URLEncode(array);
}

// Create code challenge (SHA-256 hash of verifier)
async function generateCodeChallenge(verifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return base64URLEncode(new Uint8Array(hash));
}

// Base64URL encoding (no padding, URL-safe)
function base64URLEncode(buffer) {
  return btoa(String.fromCharCode(...buffer))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// Generate both values
const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// Store codeVerifier securely (session storage for SPAs, server session for web apps)
sessionStorage.setItem('pkce_verifier', codeVerifier);

Step 2: Build Authorization Request

function buildAuthorizationURL() {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: 'your_client_id',
    redirect_uri: 'https://yourapp.com/callback',
    scope: 'openid profile email read:data',
    state: generateRandomString(32),  // CSRF protection
    nonce: generateRandomString(32),  // ID token replay protection
    code_challenge: codeChallenge,
    code_challenge_method: 'S256'
  });

  // Store state for validation
  sessionStorage.setItem('oauth_state', params.get('state'));
  sessionStorage.setItem('oauth_nonce', params.get('nonce'));

  return `https://auth.example.com/authorize?${params}`;
}

// Redirect user to authorization endpoint
window.location.href = buildAuthorizationURL();

Step 3: Handle Callback and Exchange Code

async function handleCallback() {
  const params = new URLSearchParams(window.location.search);

  // Validate state to prevent CSRF
  const storedState = sessionStorage.getItem('oauth_state');
  if (params.get('state') !== storedState) {
    throw new Error('State mismatch - possible CSRF attack');
  }

  // Check for errors
  if (params.has('error')) {
    throw new Error(`OAuth error: ${params.get('error_description')}`);
  }

  // Get authorization code
  const code = params.get('code');
  const codeVerifier = sessionStorage.getItem('pkce_verifier');

  // Exchange code for tokens
  const tokens = await exchangeCode(code, codeVerifier);

  // Clean up
  sessionStorage.removeItem('oauth_state');
  sessionStorage.removeItem('oauth_nonce');
  sessionStorage.removeItem('pkce_verifier');

  return tokens;
}

async function exchangeCode(code, codeVerifier) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: 'your_client_id',
      code: code,
      redirect_uri: 'https://yourapp.com/callback',
      code_verifier: codeVerifier
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Token exchange failed: ${error.error_description}`);
  }

  return response.json();
  // Returns: { access_token, refresh_token, id_token, expires_in, token_type }
}

Step 4: Validate ID Token (OIDC)

async function validateIdToken(idToken, nonce) {
  // Decode token (without verification for inspection)
  const parts = idToken.split('.');
  const header = JSON.parse(atob(parts[0]));
  const payload = JSON.parse(atob(parts[1]));

  // Get signing keys from IdP
  const jwksResponse = await fetch('https://auth.example.com/.well-known/jwks.json');
  const jwks = await jwksResponse.json();

  // Find the key matching the token's kid
  const key = jwks.keys.find(k => k.kid === header.kid);
  if (!key) {
    throw new Error('Signing key not found');
  }

  // Verify signature (use a library like jose)
  const isValid = await verifySignature(idToken, key);
  if (!isValid) {
    throw new Error('Invalid signature');
  }

  // Validate claims
  const now = Math.floor(Date.now() / 1000);

  if (payload.iss !== 'https://auth.example.com') {
    throw new Error('Invalid issuer');
  }

  if (payload.aud !== 'your_client_id') {
    throw new Error('Invalid audience');
  }

  if (payload.exp < now) {
    throw new Error('Token expired');
  }

  if (payload.iat > now + 60) {  // Allow 60s clock skew
    throw new Error('Token issued in the future');
  }

  if (payload.nonce !== nonce) {
    throw new Error('Invalid nonce - possible replay attack');
  }

  return payload;  // User identity information
}

Client Credentials Flow

For server-to-server authentication without user involvement.

async function getClientCredentialsToken() {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      // Client authentication via Basic auth or in body
      'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'read:data write:data'
    })
  });

  const { access_token, expires_in } = await response.json();

  // Cache token until near expiration
  return access_token;
}

// Example: Service-to-service API call
async function callInternalAPI() {
  const token = await getClientCredentialsToken();

  const response = await fetch('https://api.internal.com/data', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });

  return response.json();
}

Token Management Best Practices

Secure Token Storage

PlatformAccess TokenRefresh Token
Web (SSR)HttpOnly cookieHttpOnly cookie (separate)
SPAMemory onlyHttpOnly cookie via BFF
MobileSecure storageKeychain/Keystore
ServerEncrypted DB/secrets managerSame, with higher protection

Token Refresh with Rotation

async function refreshAccessToken(refreshToken) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: 'your_client_id',
      refresh_token: refreshToken
    })
  });

  if (!response.ok) {
    if (response.status === 400) {
      // Refresh token invalid/expired - require re-authentication
      throw new Error('Session expired - please login again');
    }
    throw new Error('Token refresh failed');
  }

  const tokens = await response.json();

  // IMPORTANT: Save the NEW refresh token (rotation)
  // Old refresh token is now invalid
  await saveTokens(tokens.access_token, tokens.refresh_token);

  return tokens.access_token;
}

Automatic Token Refresh

class TokenManager {
  constructor() {
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }

  async getAccessToken() {
    // Check if token is expired or about to expire (60s buffer)
    if (!this.accessToken || Date.now() >= this.expiresAt - 60000) {
      await this.refresh();
    }
    return this.accessToken;
  }

  async refresh() {
    if (!this.refreshToken) {
      throw new Error('No refresh token - authentication required');
    }

    try {
      const tokens = await refreshAccessToken(this.refreshToken);
      this.setTokens(tokens);
    } catch (error) {
      // Clear tokens and redirect to login
      this.clear();
      throw error;
    }
  }

  setTokens({ access_token, refresh_token, expires_in }) {
    this.accessToken = access_token;
    this.refreshToken = refresh_token;
    this.expiresAt = Date.now() + (expires_in * 1000);
  }

  clear() {
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }
}

// Usage with fetch wrapper
const tokenManager = new TokenManager();

async function authenticatedFetch(url, options = {}) {
  const token = await tokenManager.getAccessToken();

  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`
    }
  });
}

Security Checklist

Authorization Request

  • Use PKCE (code_challenge + code_challenge_method=S256)
  • Include state parameter (random, >= 32 bytes)
  • Include nonce parameter for OIDC (random, >= 32 bytes)
  • Use exact redirect_uri matching
  • Request minimum necessary scopes

Token Exchange

  • Validate state parameter matches stored value
  • Use code_verifier with PKCE
  • Exchange code promptly (codes expire quickly)
  • Use HTTPS for all token endpoint calls

Token Validation

  • Verify signature using JWKS
  • Validate iss (issuer) claim
  • Validate aud (audience) claim
  • Check exp (expiration) claim
  • Validate nonce matches for ID tokens
  • Use clock skew tolerance (e.g., 60 seconds)

Token Storage

  • Never store tokens in localStorage (XSS vulnerable)
  • Use HttpOnly cookies for web apps
  • Use platform secure storage for mobile
  • Encrypt tokens at rest on servers

Token Lifecycle

  • Implement refresh token rotation
  • Set reasonable expiration times
  • Support token revocation
  • Clear tokens on logout

Common Pitfalls

MistakeRiskSolution
Skipping PKCECode interceptionAlways use PKCE, even for confidential clients
Tokens in localStorageXSS theftUse HttpOnly cookies or memory
Long-lived access tokensExtended exposureShort expiry (15 min) + refresh tokens
No state parameterCSRF attacksRandom state, validate on callback
Trusting token claimsForged tokensAlways verify signatures
Refresh token reuseUndetected theftImplement rotation

Next Steps

Need Expert IT & Security Guidance?

Our team is ready to help protect and optimize your business technology infrastructure.