Overview

OAuth 2.0 is the standard protocol for delegated authorization. It lets users grant an application limited access to their data on another service without sharing their password. This tutorial explains the roles, the authorization code flow with PKCE, and the mistakes that lead to real vulnerabilities.

The Four Roles

RoleDescriptionExample
Resource ownerThe user who owns the dataYou
ClientThe application requesting accessA calendar app
Authorization serverIssues tokens after user consentGoogle Identity Platform
Resource serverHosts the protected dataGoogle Calendar API

Common Grant Types

GrantUse caseStatus
Authorization Code + PKCEWeb apps, mobile apps, SPAsRecommended
Client CredentialsMachine-to-machine, no userRecommended
Device CodeDevices without a browserRecommended
ImplicitLegacy SPAsDeprecated
Resource Owner PasswordLegacy first-party appsDeprecated

Authorization Code Flow with PKCE

PKCE (Proof Key for Code Exchange, pronounced "pixy") protects public clients that cannot keep a secret. It replaces the client secret with a dynamically generated value.

  1. The client generates a random code_verifier.
  2. It hashes the verifier with SHA-256 to produce a code_challenge.
  3. The client redirects the user to the authorization endpoint with the challenge.
  4. After consent, the authorization server redirects back with an authorization code.
  5. The client exchanges the code plus the original code_verifier for tokens.

Step 1: Generate the PKCE Values

// Browser / Node.js
function base64UrlEncode(buffer) {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

async function generatePkce() {
  const verifierBytes = crypto.getRandomValues(new Uint8Array(32));
  const verifier = base64UrlEncode(verifierBytes);

  const digest = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(verifier)
  );
  const challenge = base64UrlEncode(digest);

  return { verifier, challenge };
}

Step 2: Redirect to the Authorization Endpoint

const params = new URLSearchParams({
  response_type: 'code',
  client_id: 'my-client-id',
  redirect_uri: 'https://app.example.com/callback',
  scope: 'openid profile email',
  state: crypto.randomUUID(),
  code_challenge: challenge,
  code_challenge_method: 'S256',
});

window.location.href = `https://auth.example.com/authorize?${params}`;

The state parameter prevents CSRF. Store it in session storage and verify it on the callback.

Step 3: Exchange the Code for Tokens

// Server-side exchange
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',
    code: receivedCode,
    redirect_uri: 'https://app.example.com/callback',
    client_id: 'my-client-id',
    code_verifier: storedVerifier,
  }),
});

const tokens = await response.json();
// {
//   access_token: "...",
//   refresh_token: "...",
//   token_type: "Bearer",
//   expires_in: 3600,
//   id_token: "..."
// }

Step 4: Call the Resource Server

const profile = await fetch('https://api.example.com/userinfo', {
  headers: { Authorization: `Bearer ${tokens.access_token}` },
});

Token Types

TokenPurposeFormat
Access tokenAuthorizes API callsOpaque or JWT
Refresh tokenObtains new access tokens without user interactionOpaque
ID tokenCarries user identity claims (OpenID Connect)JWT

Keep access tokens short-lived (5–60 minutes). Refresh tokens are long-lived and must be stored server-side or in secure HttpOnly cookies for browser clients.

Scopes and Consent

Scopes define what the client is asking for. Request the minimum set needed:

scope=openid profile email

Do not request broad scopes like drive if you only need to read a specific folder. Users are more likely to approve minimal requests, and reviewers at providers like Google will reject over-broad apps.

Where to Store Tokens

StorageXSS riskCSRF riskVerdict
localStorageHighLowAvoid for refresh tokens
sessionStorageHighLowSame as above
HttpOnly cookieLowMediumBest for refresh tokens, add SameSite
In-memory (JS variable)LowLowBest for access tokens in SPAs

Security Mistakes to Avoid

  • Missing state validation. Enables CSRF on the callback endpoint.
  • Open redirect on redirect_uri. Attackers can steal authorization codes. Always whitelist exact URIs.
  • Storing tokens in localStorage. Any XSS can exfiltrate them.
  • Long-lived access tokens. A single leak becomes a long-term compromise.
  • Trusting the id_token without verifying the signature. Always validate against the provider's JWKS endpoint.
  • Using the implicit flow. Deprecated for a reason: tokens appear in URL fragments and browser history.

Tools for Testing