Skip to main content
🔐Security

OAuth 2.0: The Complete Guide to Authorization Flows and Security

OAuth 2.0 is the industry-standard protocol for authorization. It enables applications to obtain limited access to user accounts on third-party services wi...

📖 8 min read

OAuth 2.0: The Complete Guide to Authorization Flows and Security

OAuth 2.0 is the industry-standard protocol for authorization. It enables applications to obtain limited access to user accounts on third-party services without exposing passwords. Every time you click "Sign in with Google" or "Connect with GitHub," OAuth 2.0 is working behind the scenes. This guide covers all grant types, token management, OpenID Connect, and security best practices essential for system design interviews.

OAuth 2.0 Roles

Role Description Example
Resource Owner The user who authorizes access to their data You, the end user
Client The application requesting access A third-party app (Spotify, Slack)
Authorization Server Authenticates the user and issues tokens Google Accounts, Auth0, Okta
Resource Server Hosts the protected resources (APIs) Google Calendar API, GitHub API

Grant Types Overview

Grant Type Best For Confidential Client? User Interaction?
Authorization Code Server-side web apps Yes Yes
Authorization Code + PKCE SPAs, mobile apps, native apps No Yes
Client Credentials Service-to-service (M2M) Yes No
Device Code Smart TVs, CLI tools, IoT No Yes (on another device)
Implicit (Deprecated) Legacy SPAs (use PKCE instead) No Yes

Authorization Code Flow

The most secure and common flow for server-side applications. The authorization code is exchanged for tokens via a back-channel (server-to-server), keeping tokens out of the browser.

Step-by-Step Flow

  1. User clicks "Login with Provider" — Client redirects to authorization server
  2. User authenticates — Enters credentials on the authorization server's login page
  3. User grants consent — Approves the requested scopes
  4. Authorization server redirects back — Returns an authorization code to the client's redirect URI
  5. Client exchanges code for tokens — Server-to-server POST request with the code
  6. Authorization server returns tokens — Access token + refresh token

Authorization Request

GET /authorize?
  response_type=code
  &client_id=abc123
  &redirect_uri=https://myapp.com/callback
  &scope=openid profile email
  &state=xyzRandom123
  HTTP/1.1
Host: auth.provider.com

Token Exchange Request

POST /token HTTP/1.1
Host: auth.provider.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTHORIZATION_CODE_HERE
&redirect_uri=https://myapp.com/callback
&client_id=abc123
&client_secret=secret456

Token Response

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
  "scope": "openid profile email",
  "id_token": "eyJhbGciOiJSUzI1NiIs..."
}

Authorization Code Flow with PKCE

PKCE (Proof Key for Code Exchange) adds an extra security layer for public clients like SPAs and mobile apps that cannot securely store a client secret. It prevents authorization code interception attacks.

const crypto = require('crypto');

// Step 1: Generate code verifier (random string)
function generateCodeVerifier() {
  return crypto.randomBytes(32)
    .toString('base64url'); // 43-character URL-safe string
}

// Step 2: Generate code challenge (SHA-256 hash of verifier)
function generateCodeChallenge(verifier) {
  return crypto.createHash('sha256')
    .update(verifier)
    .digest('base64url');
}

// Usage
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);

// Include in authorization request:
// &code_challenge=CODE_CHALLENGE
// &code_challenge_method=S256

// Include verifier in token exchange:
// &code_verifier=CODE_VERIFIER

The authorization request includes the code_challenge. When exchanging the code for tokens, the client sends the original code_verifier. The server hashes it and compares — only the original client can complete the exchange.

Client Credentials Flow

Used for machine-to-machine communication where no user is involved. The client authenticates directly with its own credentials.

// Service-to-service token request
async function getM2MToken() {
  const response = await fetch('https://auth.provider.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET,
      scope: 'api:read api:write'
    })
  });

  const data = await response.json();
  return data.access_token; // No refresh token issued
}

Device Code Flow

Designed for devices with limited input capabilities (smart TVs, game consoles, CLI tools). The device displays a code, and the user completes authentication on another device like a phone or computer.

// Step 1: Device requests codes
// POST /device/code
// Response: { device_code, user_code, verification_uri, interval }

// Step 2: Display to user
console.log(`Visit ${verification_uri} and enter code: ${user_code}`);

// Step 3: Poll for completion
async function pollForToken(deviceCode, interval) {
  while (true) {
    await sleep(interval * 1000);
    const response = await fetch('https://auth.provider.com/token', {
      method: 'POST',
      body: new URLSearchParams({
        grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
        device_code: deviceCode,
        client_id: CLIENT_ID
      })
    });
    const data = await response.json();

    if (data.access_token) return data;
    if (data.error === 'expired_token') throw new Error('Code expired');
    // 'authorization_pending' or 'slow_down' — keep polling
  }
}

Access Tokens vs Refresh Tokens

Feature Access Token Refresh Token
Purpose Access protected resources Obtain new access tokens
Lifetime Short (15 min - 1 hour) Long (days - months)
Sent To Resource server (API) Authorization server only
Storage Memory (preferred) or secure cookie HttpOnly secure cookie or secure storage
Revocable Not easily (if JWT) Yes (server-side check)

Token Refresh Implementation

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

  if (!response.ok) {
    // Refresh token expired or revoked — redirect to login
    redirectToLogin();
    return null;
  }

  const data = await response.json();
  // Implement refresh token rotation for security
  storeTokens(data.access_token, data.refresh_token);
  return data.access_token;
}

Learn more about JWT token structure and how access tokens encode claims for authorization decisions.

OpenID Connect (OIDC)

OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth 2.0 handles authorization (access to resources), OIDC adds authentication (identity verification).

OIDC introduces the ID Token, a JWT that contains user identity claims:

// Decoded ID Token payload
{
  "iss": "https://auth.provider.com",
  "sub": "user123",           // Unique user identifier
  "aud": "abc123",            // Your client ID
  "exp": 1700000000,
  "iat": 1699996400,
  "nonce": "randomNonce",     // Prevents replay attacks
  "name": "Jane Developer",
  "email": "jane@example.com",
  "email_verified": true,
  "picture": "https://example.com/jane.jpg"
}

OIDC Scopes

Scope Claims Returned
openid sub (required for OIDC)
profile name, family_name, given_name, picture
email email, email_verified
address formatted address, street_address, locality
phone phone_number, phone_number_verified

Security Best Practices

  • Always use PKCE — Even for confidential clients, PKCE adds protection. It is mandatory for public clients.
  • Validate the state parameter — Prevents CSRF attacks. Generate a random state, store it in session, and verify it in the callback.
  • Use short-lived access tokens — 15 minutes is a good default. Use refresh tokens for longer sessions.
  • Implement token rotation — Issue a new refresh token with every refresh, and invalidate the old one.
  • Validate redirect URIs strictly — Use exact string matching. Never allow open redirects.
  • Store tokens securely — Use HttpOnly, Secure, SameSite cookies. Never store tokens in localStorage.
  • Use TLS encryption — All OAuth 2.0 communication must use HTTPS.
  • Validate ID tokens — Check issuer, audience, expiration, and signature. Verify the nonce to prevent replay attacks.

Test OAuth flows and inspect tokens with our API and Network Tools, and explore Security Crypto Tools for JWT debugging.

Common Vulnerabilities and Mitigations

Vulnerability Attack Mitigation
Authorization code interception Attacker intercepts code from redirect Use PKCE
CSRF on callback Attacker injects their auth code Validate state parameter
Open redirect Malicious redirect_uri steals code Exact redirect URI matching
Token leakage Tokens in URL fragments or logs Use auth code flow, not implicit
Refresh token theft Stolen refresh token used indefinitely Refresh token rotation + binding

Frequently Asked Questions

When should I use OAuth 2.0 vs simple API keys?

Use API keys for server-to-server communication where you control both sides and need simple identification. Use OAuth 2.0 when third-party applications need delegated access to user data, when you need fine-grained scopes, or when user consent is required. OAuth 2.0 is also better for user-facing applications because tokens can be scoped and revoked.

Is the implicit grant type still safe to use?

No. The implicit grant is deprecated in the OAuth 2.0 Security Best Current Practice (RFC). It exposes access tokens in the URL fragment, making them vulnerable to interception. Always use Authorization Code flow with PKCE for browser-based applications instead.

What is the difference between OAuth 2.0 and OAuth 2.1?

OAuth 2.1 is a consolidation of OAuth 2.0 with its security best practice RFCs. Key changes include: PKCE is required for all authorization code grants, the implicit grant is removed, refresh tokens must be sender-constrained or one-time use, and bearer tokens in query strings are prohibited. If starting fresh, design for OAuth 2.1 compliance.

How do I choose the right grant type?

Server-rendered web app with a backend: Authorization Code. SPA or mobile app: Authorization Code + PKCE. Backend service calling another API: Client Credentials. Smart TV or CLI tool: Device Code. If you are unsure, Authorization Code + PKCE is the safest default for any user-facing scenario.

How does OAuth 2.0 work with microservices?

In a microservices architecture, the API gateway typically validates access tokens and forwards user claims to downstream services. Services can use client credentials for service-to-service calls. A centralized authorization server issues all tokens, and each service validates them independently. Use our tools to explore these patterns interactively.

Related Articles