Understanding OAuth: A Complete Guide

security · oauth · web|2025-11-21 · 16 min read

Table of Contents

  1. The Problem OAuth Solves
  2. Real-World Analogy
  3. Key Concepts & Terminology
  4. How OAuth Actually Works
  5. OAuth Flows Explained
  6. Security Features
  7. Common Misconceptions
  8. Practical Examples
  9. Best Practices

The Problem OAuth Solves

Before OAuth: The Password Anti-Pattern

Imagine you want to let a third-party app access your Gmail to send emails on your behalf. Before OAuth, you had two bad options:

Option 1: Share Your Password

You → Give password to third-party app
App → Log in as YOU to Gmail
Problem: App has FULL access to everything you can do
Problem: You can't revoke access without changing password
Problem: If app is compromised, attacker has your password

Option 2: Create Per-App Passwords 😐

Gmail → Generate special password for each app
You → Give app-specific password to third-party app
Better: Each app has unique credentials
Problem: Still full access to your account
Problem: Tedious to manage many passwords

The OAuth Solution ✅

OAuth lets you grant limited, revocable access to your resources without sharing your password:

You → Authorize third-party app via OAuth
App → Gets temporary token with specific permissions
Result: App can ONLY do what you authorized
Result: You can revoke access anytime
Result: Your password stays secret

Key Principle: OAuth separates authentication (who you are) from authorization (what you're allowed to do).


Real-World Analogy

The Hotel Key Card Analogy

Think of OAuth like a hotel giving you a key card:

Traditional Password Approach = Master Key

  • Hotel gives you the same master key the manager uses
  • Opens every door, office, and safe
  • If you lose it, hotel has to change all locks
  • If you lend it to a friend, they can access everything

OAuth Approach = Limited Key Card

  • Hotel gives you a key card programmed for YOUR room only
  • Works only during your stay (expires automatically)
  • Can access pool and gym (specific scopes)
  • Can't access other rooms, offices, or safes
  • If lost, hotel deactivates that card without affecting others
  • You can have multiple cards (tokens) for different people/purposes

OAuth Terminology:

  • Resource Owner: You (the hotel guest)
  • Resource Server: The hotel rooms, gym, pool (your data)
  • Client: Your friend who needs temporary access
  • Authorization Server: The hotel front desk (issues key cards)
  • Access Token: The key card itself
  • Scope: What the card can access (room, gym, pool)
  • Expiration: When the card stops working

Key Concepts & Terminology

The Four Roles

1. Resource Owner (User)

  • Who: The actual person (you)
  • Controls: Their data and permissions
  • Example: You, the Gmail user

2. Client (Application)

  • Who: The third-party app requesting access
  • Wants: To access user's data
  • Example: A scheduling app that wants to send emails via your Gmail

3. Authorization Server

  • Who: The system that handles login and permissions
  • Issues: Access tokens after user approves
  • Example: Google's OAuth server at accounts.google.com

4. Resource Server

  • Who: The system that holds the protected data
  • Validates: Access tokens before serving data
  • Example: Gmail API servers

Often the Authorization Server and Resource Server are the same system, but conceptually they're different roles.

Important Concepts

Access Token

  • Short-lived credential (usually 1 hour)
  • Like a temporary key card
  • Presented with each API request
  • Format: Often a JWT (JSON Web Token) or opaque string
  • Example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Refresh Token

  • Long-lived credential (days/weeks/months)
  • Used to get new access tokens when they expire
  • Like going back to hotel desk for a new key card
  • Stored securely, never sent to resource server
  • Can be revoked by user anytime

Scope

  • Specific permissions being requested
  • Like "read-only emails" vs "send emails"
  • User sees and approves these during authorization
  • Examples: gmail.readonly, gmail.send, profile.email

Authorization Code

  • Temporary code issued after user approves
  • Exchanged for access token
  • Single-use, expires quickly (usually 10 minutes)
  • Proof that user said "yes"

Redirect URI

  • Where authorization server sends user after approval
  • Must be pre-registered for security
  • Example: https://yourapp.com/oauth/callback

How OAuth Actually Works

The Big Picture

┌─────────────┐          ┌──────────────────┐          ┌─────────────┐
│   User      │          │  Your App        │          │  Google     │
│  (You)      │          │  (Client)        │          │  (Auth +    │
│             │          │                  │          │   Resource) │
└──────┬──────┘          └────────┬─────────┘          └──────┬──────┘
       │                          │                           │
       │  1. Click "Sign in       │                           │
       │     with Google"         │                           │
       │─────────────────────────>│                           │
       │                          │                           │
       │                          │  2. Redirect to Google    │
       │                          │     with app info         │
       │                          │──────────────────────────>│
       │                          │                           │
       │  3. Login page           │                           │
       │  (Gmail + password)      │                           │
       │<─────────────────────────────────────────────────────│
       │                          │                           │
       │  4. Enter credentials    │                           │
       │─────────────────────────────────────────────────────>│
       │                          │                           │
       │  5. Consent screen       │                           │
       │  "Allow app to read      │                           │
       │   your email?"           │                           │
       │<─────────────────────────────────────────────────────│
       │                          │                           │
       │  6. Approve              │                           │
       │─────────────────────────────────────────────────────>│
       │                          │                           │
       │  7. Redirect back to app │                           │
       │     with code            │                           │
       │<─────────────────────────────────────────────────────│
       │                          │                           │
       │  8. Browser redirects    │                           │
       │     with code            │                           │
       │─────────────────────────>│                           │
       │                          │                           │
       │                          │  9. Exchange code for     │
       │                          │     access token          │
       │                          │──────────────────────────>│
       │                          │                           │
       │                          │ 10. Return access token   │
       │                          │<──────────────────────────│
       │                          │                           │
       │ 11. App uses token to    │ 12. Validate token &      │
       │     call Gmail API       │     return data           │
       │                          │──────────────────────────>│
       │                          │<──────────────────────────│
       │                          │                           │
       │ 13. Show user their      │                           │
       │     emails               │                           │
       │<─────────────────────────│                           │

Step-by-Step Breakdown

Step 1: User Initiates

  • User clicks "Sign in with Google" button in your app
  • Your app recognizes user wants to authorize

Step 2: Redirect to Authorization Server Your app redirects user to Google with this URL:

https://accounts.google.com/o/oauth2/v2/auth?
  client_id=YOUR_APP_ID
  &redirect_uri=https://yourapp.com/callback
  &response_type=code
  &scope=gmail.readonly profile.email
  &state=random_string_for_security

Step 3-4: User Authenticates

  • Google shows login page
  • User enters their Gmail and password
  • Google verifies credentials

Step 5-6: User Authorizes

  • Google shows consent screen: "YourApp wants to read your emails"
  • User clicks "Allow" (or "Deny")

Step 7-8: Authorization Code Issued Google redirects user back to your app:

https://yourapp.com/callback?
  code=AUTH_CODE_HERE
  &state=random_string_for_security

Step 9-10: Exchange Code for Token Your app (backend) makes a server-to-server call:

POST https://oauth2.googleapis.com/token
Content-Type: application/json
 
{
  "client_id": "YOUR_APP_ID",
  "client_secret": "YOUR_APP_SECRET",
  "code": "AUTH_CODE_HERE",
  "redirect_uri": "https://yourapp.com/callback",
  "grant_type": "authorization_code"
}

Google responds with:

{
  "access_token": "ya29.a0AfH6SMB...",
  "refresh_token": "1//0gLHT...",
  "expires_in": 3600,
  "scope": "gmail.readonly profile.email",
  "token_type": "Bearer"
}

Step 11-12: Use Access Token Your app calls Gmail API:

GET https://gmail.googleapis.com/gmail/v1/users/me/messages
Authorization: Bearer ya29.a0AfH6SMB...

Gmail validates token and returns data:

{
  "messages": [
    { "id": "msg1", "snippet": "Hello..." },
    { "id": "msg2", "snippet": "Meeting at..." }
  ]
}

Step 13: Show User Data Your app displays the user's emails in your UI.


OAuth Flows Explained

OAuth has different "flows" (also called "grant types") for different scenarios. Think of them as different recipes for getting an access token.

1. Authorization Code Flow (Most Common)

When to Use:

  • Traditional web apps (with backend server)
  • Server can securely store client_secret
  • Most secure option

Key Feature: Uses two steps (authorization code → access token) for extra security.

Flow:

User → Redirected to auth server → Logs in → Approves
     ← Returns with authorization code
Your Backend → Exchanges code + secret for token
     ← Gets access token + refresh token

Security: Authorization code is useless without client_secret, which only your backend knows.

Example Use Case: A web app where users sign in with Google/GitHub/etc.

2. Authorization Code Flow with PKCE (Public Clients)

When to Use:

  • Mobile apps (iOS, Android)
  • Single-page apps (React, Vue, Angular)
  • Any app that can't securely store a secret

Key Feature: Uses PKCE (Proof Key for Code Exchange) to secure the flow without needing client_secret.

What is PKCE?

Before starting OAuth flow, app generates two values:

Code Verifier: Random string

Example: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

Code Challenge: Hash of code verifier

Base64URL(SHA256(code_verifier))
Example: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM

Flow:

1. App generates code_verifier (keep secret in app)
2. App generates code_challenge from verifier
3. User → Redirect to auth server with code_challenge
4. User logs in and approves
5. User ← Returns with authorization code
6. App → Exchange code + code_verifier for token
7. Auth server verifies verifier matches challenge
8. App ← Gets access token

Security: Even if someone intercepts the authorization code, they can't exchange it without the code_verifier (which never left the app).

Why It's Needed: Mobile and single-page apps can't securely store a client_secret (anyone can decompile the app or inspect browser code).

3. Client Credentials Flow (Machine-to-Machine)

When to Use:

  • Backend service calling another backend service
  • No user involved
  • Server-to-server communication

Flow:

Your Service → Sends client_id + client_secret
            ← Gets access token
Your Service → Calls API with token
            ← Gets data

Example Use Case: A nightly batch job that syncs data between two systems.

Security: Both systems trust each other, no user consent needed.

4. Implicit Flow (Deprecated) ⚠️

Status: DEPRECATED - Don't use this!

Why It Existed: For single-page apps before PKCE was invented.

Problem: Returns access token directly in URL (insecure).

Instead Use: Authorization Code Flow with PKCE.

5. Resource Owner Password Credentials (Legacy) ⚠️

Status: LEGACY - Avoid if possible!

How It Works: User gives username/password directly to your app, app exchanges for token.

Problem: Defeats the purpose of OAuth (app sees user's password).

When It's Used: First-party apps only (like the Gmail mobile app itself).


Security Features

1. State Parameter (CSRF Protection)

Problem: Attacker tricks user into authorizing attacker's app.

Solution: State parameter

How It Works:

1. Your app generates random string: state = "xyz789"
2. Stores in session: session['oauth_state'] = "xyz789"
3. Includes in OAuth URL: &state=xyz789
4. Auth server returns same state in callback
5. Your app verifies: callback_state == session['oauth_state']
6. If mismatch → reject (possible CSRF attack)

Why It Matters: Prevents attacker from completing OAuth flow they didn't initiate.

2. Redirect URI Whitelist

Problem: Attacker registers malicious redirect URI to steal authorization code.

Solution: Pre-register allowed redirect URIs

How It Works:

1. Developer registers app with auth server
2. Specifies exact redirect URIs allowed:
   - https://yourapp.com/callback
   - https://yourapp.com/oauth/google
3. Auth server ONLY redirects to these URIs
4. Attacker can't use https://evil.com/steal

Why It Matters: Ensures authorization code only goes to legitimate app.

3. PKCE (Proof Key for Code Exchange)

Problem: Mobile apps can't securely store client_secret, making authorization code vulnerable.

Solution: Cryptographic proof that code exchanger is same as code requester

How It Works:

1. App generates code_verifier (random string)
2. App computes code_challenge = SHA256(code_verifier)
3. Authorization request includes code_challenge
4. Token exchange includes code_verifier
5. Server verifies SHA256(code_verifier) == code_challenge
6. Only matching verifier gets token

Why It Matters: Even if attacker intercepts authorization code, they can't use it without the code_verifier.

4. Short-Lived Access Tokens

Problem: If token is stolen, attacker has indefinite access.

Solution: Tokens expire quickly (typically 1 hour)

How It Works:

Access Token:  Expires in 1 hour  → Limited blast radius if stolen
Refresh Token: Expires in 30 days → Used to get new access tokens
                                  → Can be revoked by user anytime

Why It Matters: Limits damage from stolen token; user can revoke refresh token.

5. Token Scopes

Problem: App gets more access than needed (violates least-privilege principle).

Solution: Request only necessary scopes

How It Works:

Bad:  scope=all.permissions
Good: scope=gmail.readonly profile.email

User sees: "App wants to read your emails and see your email address"
User can deny if suspicious

Why It Matters: Principle of least privilege; users can see exactly what app gets.

6. HTTPs Everywhere

Problem: Tokens transmitted over unencrypted connection can be intercepted.

Solution: OAuth REQUIRES HTTPS for all communications (except localhost in development).

Why It Matters: Prevents man-in-the-middle attacks stealing tokens.


Common Misconceptions

❌ "OAuth is for Authentication"

Truth: OAuth is for authorization, not authentication.

Explanation:

  • Authentication = Proving who you are ("I am John")
  • Authorization = Granting permission ("John can read emails")

OAuth tells your app what the user authorized you to do, not who they are.

Caveat: You can use OAuth for authentication by requesting the profile scope, but that's technically a layer on top of OAuth. OpenID Connect (OIDC) was created specifically for authentication built on OAuth.

Truth: They're different concepts with different purposes.

Session Cookie:

  • Identifies user to YOUR application
  • Managed by YOUR server
  • Used for YOUR app's authentication

Access Token:

  • Grants access to THIRD-PARTY resources
  • Managed by THIRD-PARTY server
  • Used for API calls to THIRD-PARTY

Example: After OAuth, you might create a session cookie to keep user logged into YOUR app, while using the access token to call Gmail API on their behalf.

❌ "Client Secret Must Be Secret Everywhere"

Truth: Only confidential clients need client_secret.

Public Clients (mobile, SPA):

  • Can't securely store secrets
  • Use PKCE instead
  • Don't need client_secret

Confidential Clients (backend servers):

  • Can securely store secrets
  • Must keep client_secret private
  • Use Authorization Code Flow

❌ "Refresh Tokens Are Optional"

Truth: For long-running apps, refresh tokens are essential.

Without Refresh Token:

  • User re-authorizes every hour (bad UX)
  • Or you store user's password (security nightmare)

With Refresh Token:

  • App silently gets new access tokens
  • User stays logged in seamlessly
  • User can revoke anytime

Practical Examples

Example 1: Sign in with Google (Web App)

Your Stack: React frontend + Node.js backend

Step 1: Setup (One-Time)

// 1. Register app at Google Cloud Console
// 2. Get client_id and client_secret
// 3. Set redirect URI: https://yourapp.com/auth/google/callback
 
const config = {
  clientId: 'YOUR_CLIENT_ID',
  clientSecret: 'YOUR_CLIENT_SECRET',
  redirectUri: 'https://yourapp.com/auth/google/callback',
  authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  scopes: ['openid', 'profile', 'email']
};

Step 2: Frontend - Initiate OAuth

// User clicks "Sign in with Google"
function handleGoogleLogin() {
  // Generate random state for CSRF protection
  const state = generateRandomString();
  sessionStorage.setItem('oauth_state', state);
 
  // Build authorization URL
  const authUrl = new URL(config.authUrl);
  authUrl.searchParams.append('client_id', config.clientId);
  authUrl.searchParams.append('redirect_uri', config.redirectUri);
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('scope', config.scopes.join(' '));
  authUrl.searchParams.append('state', state);
 
  // Redirect user to Google
  window.location.href = authUrl.toString();
}

Step 3: Backend - Handle Callback

// Express route handler
app.get('/auth/google/callback', async (req, res) => {
  const { code, state } = req.query;
 
  // Verify state matches (CSRF protection)
  const savedState = req.session.oauth_state;
  if (state !== savedState) {
    return res.status(400).send('Invalid state');
  }
 
  try {
    // Exchange code for token
    const tokenResponse = await fetch(config.tokenUrl, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        client_id: config.clientId,
        client_secret: config.clientSecret,
        code: code,
        redirect_uri: config.redirectUri,
        grant_type: 'authorization_code'
      })
    });
 
    const tokens = await tokenResponse.json();
    // tokens = { access_token, refresh_token, expires_in }
 
    // Get user profile
    const profileResponse = await fetch(
      'https://www.googleapis.com/oauth2/v2/userinfo',
      {
        headers: { Authorization: `Bearer ${tokens.access_token}` }
      }
    );
    const profile = await profileResponse.json();
    // profile = { email, name, picture }
 
    // Store tokens securely (encrypted in database)
    await storeUserTokens(profile.email, tokens);
 
    // Create session for user
    req.session.userId = profile.email;
 
    // Redirect to app
    res.redirect('/dashboard');
  } catch (error) {
    res.status(500).send('Authentication failed');
  }
});

Step 4: Use Access Token

// Later, when calling Gmail API
async function getUserEmails(userId) {
  // Retrieve stored tokens
  const tokens = await getStoredTokens(userId);
 
  // Check if access token expired
  if (isTokenExpired(tokens.expires_at)) {
    // Refresh token
    tokens = await refreshAccessToken(tokens.refresh_token);
    await updateStoredTokens(userId, tokens);
  }
 
  // Call Gmail API
  const response = await fetch(
    'https://gmail.googleapis.com/gmail/v1/users/me/messages',
    {
      headers: { Authorization: `Bearer ${tokens.access_token}` }
    }
  );
 
  return response.json();
}
 
async function refreshAccessToken(refreshToken) {
  const response = await fetch(config.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: config.clientId,
      client_secret: config.clientSecret,
      refresh_token: refreshToken,
      grant_type: 'refresh_token'
    })
  });
 
  return response.json();
}

Example 2: Mobile App with PKCE

Your Stack: React Native mobile app

Step 1: Setup

const config = {
  clientId: 'YOUR_CLIENT_ID',
  // Note: NO client_secret for mobile apps!
  redirectUri: 'yourapp://oauth/callback',
  authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  tokenUrl: 'https://oauth2.googleapis.com/token',
  scopes: ['openid', 'profile', 'email']
};

Step 2: Generate PKCE Parameters

import * as Crypto from 'expo-crypto';
import base64 from 'base-64';
 
// Generate code verifier (random string)
function generateCodeVerifier() {
  const randomBytes = Crypto.getRandomBytes(32);
  return base64.encodeURI(String.fromCharCode(...randomBytes));
}
 
// Generate code challenge from verifier
async function generateCodeChallenge(verifier) {
  const hash = await Crypto.digestStringAsync(
    Crypto.CryptoDigestAlgorithm.SHA256,
    verifier
  );
  return base64.encodeURI(hash);
}

Step 3: Initiate OAuth with PKCE

import * as WebBrowser from 'expo-web-browser';
 
async function handleGoogleLogin() {
  // Generate PKCE parameters
  const codeVerifier = generateCodeVerifier();
  const codeChallenge = await generateCodeChallenge(codeVerifier);
 
  // Store verifier for later
  await AsyncStorage.setItem('code_verifier', codeVerifier);
 
  // Generate state
  const state = generateRandomString();
  await AsyncStorage.setItem('oauth_state', state);
 
  // Build authorization URL with PKCE
  const authUrl = new URL(config.authUrl);
  authUrl.searchParams.append('client_id', config.clientId);
  authUrl.searchParams.append('redirect_uri', config.redirectUri);
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('scope', config.scopes.join(' '));
  authUrl.searchParams.append('state', state);
  authUrl.searchParams.append('code_challenge', codeChallenge);
  authUrl.searchParams.append('code_challenge_method', 'S256');
 
  // Open browser for OAuth
  const result = await WebBrowser.openAuthSessionAsync(
    authUrl.toString(),
    config.redirectUri
  );
 
  if (result.type === 'success') {
    await handleCallback(result.url);
  }
}

Step 4: Exchange Code with PKCE

async function handleCallback(url) {
  const params = new URLSearchParams(url.split('?')[1]);
  const code = params.get('code');
  const state = params.get('state');
 
  // Verify state
  const savedState = await AsyncStorage.getItem('oauth_state');
  if (state !== savedState) {
    throw new Error('Invalid state');
  }
 
  // Get stored code verifier
  const codeVerifier = await AsyncStorage.getItem('code_verifier');
 
  // Exchange code for token (with PKCE verifier, NO client_secret)
  const response = await fetch(config.tokenUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      client_id: config.clientId,
      code: code,
      redirect_uri: config.redirectUri,
      grant_type: 'authorization_code',
      code_verifier: codeVerifier  // PKCE proof
    })
  });
 
  const tokens = await response.json();
 
  // Store tokens securely
  await SecureStore.setItemAsync('access_token', tokens.access_token);
  await SecureStore.setItemAsync('refresh_token', tokens.refresh_token);
 
  // Navigate to app
  navigation.navigate('Dashboard');
}

Best Practices

1. Storage

Access Tokens:

  • ✅ Store in memory (web apps)
  • ✅ Store in secure storage (mobile: Keychain/KeyStore)
  • ❌ Don't store in localStorage (web) - vulnerable to XSS
  • ❌ Don't store in plain text

Refresh Tokens:

  • ✅ Store encrypted in backend database
  • ✅ Store in secure storage on mobile
  • ❌ Never send to frontend (web apps)
  • ❌ Never log or expose in URLs

Client Secret:

  • ✅ Store as environment variable
  • ✅ Keep on backend only
  • ❌ Never commit to git
  • ❌ Never send to frontend

2. Security

Always Use:

  • ✅ HTTPS everywhere (except localhost dev)
  • ✅ State parameter (CSRF protection)
  • ✅ PKCE for mobile/SPA apps
  • ✅ Short-lived access tokens (1 hour)
  • ✅ Request minimum scopes needed

Never Do:

  • ❌ Log tokens
  • ❌ Put tokens in URLs
  • ❌ Store secrets in frontend code
  • ❌ Skip token expiration checks
  • ❌ Trust tokens without validation

3. User Experience

Good UX:

  • ✅ Explain why you need permissions
  • ✅ Request scopes incrementally (as needed)
  • ✅ Handle token refresh silently
  • ✅ Let users revoke access easily
  • ✅ Show which permissions are active

Bad UX:

  • ❌ Request all possible scopes upfront
  • ❌ Force re-auth when token expires
  • ❌ Vague permission requests
  • ❌ No way to see/revoke access

4. Error Handling

Common Errors:

invalid_grant

  • Meaning: Authorization code already used or expired
  • Fix: Generate new authorization flow

invalid_client

  • Meaning: Client ID or secret wrong
  • Fix: Check credentials, re-register app

invalid_scope

  • Meaning: Requested scope not allowed
  • Fix: Request valid scopes, check app permissions

access_denied

  • Meaning: User clicked "Deny"
  • Fix: Handle gracefully, explain why needed

invalid_token

  • Meaning: Access token expired or revoked
  • Fix: Use refresh token to get new access token

5. Token Management

Checklist:

  • Store refresh tokens encrypted
  • Rotate refresh tokens on use
  • Revoke old tokens when issuing new ones
  • Provide user-facing token revocation UI
  • Log token usage for audit trail
  • Monitor for suspicious activity
  • Set reasonable expiration times
  • Clean up expired tokens from database

Quick Reference

OAuth Flow Cheat Sheet

┌───────────────────────────────────────────────────────────────┐
│ STEP 1: Authorization Request                                 │
├───────────────────────────────────────────────────────────────┤
│ GET /authorize?                                               │
│   client_id=YOUR_APP                                          │
│   &redirect_uri=https://yourapp.com/callback                  │
│   &response_type=code                                         │
│   &scope=read write                                           │
│   &state=random123                                            │
│   &code_challenge=xyz789    ← PKCE (mobile/SPA only)         │
│   &code_challenge_method=S256                                 │
└───────────────────────────────────────────────────────────────┘
                              ↓
┌───────────────────────────────────────────────────────────────┐
│ STEP 2: User Approves                                         │
├───────────────────────────────────────────────────────────────┤
│ [User logs in and clicks "Allow"]                             │
└───────────────────────────────────────────────────────────────┘
                              ↓
┌───────────────────────────────────────────────────────────────┐
│ STEP 3: Authorization Code Issued                             │
├───────────────────────────────────────────────────────────────┤
│ Redirect to:                                                  │
│ https://yourapp.com/callback?                                 │
│   code=AUTH_CODE                                              │
│   &state=random123                                            │
└───────────────────────────────────────────────────────────────┘
                              ↓
┌───────────────────────────────────────────────────────────────┐
│ STEP 4: Token Exchange                                        │
├───────────────────────────────────────────────────────────────┤
│ POST /token                                                   │
│ {                                                             │
│   "grant_type": "authorization_code",                         │
│   "code": "AUTH_CODE",                                        │
│   "redirect_uri": "https://yourapp.com/callback",             │
│   "client_id": "YOUR_APP",                                    │
│   "client_secret": "SECRET",   ← Confidential clients only   │
│   "code_verifier": "xyz789"    ← PKCE (mobile/SPA) only      │
│ }                                                             │
└───────────────────────────────────────────────────────────────┘
                              ↓
┌───────────────────────────────────────────────────────────────┐
│ STEP 5: Access Token Returned                                 │
├───────────────────────────────────────────────────────────────┤
│ {                                                             │
│   "access_token": "ya29.a0...",                               │
│   "refresh_token": "1//0gL...",                               │
│   "expires_in": 3600,                                         │
│   "token_type": "Bearer",                                     │
│   "scope": "read write"                                       │
│ }                                                             │
└───────────────────────────────────────────────────────────────┘
                              ↓
┌───────────────────────────────────────────────────────────────┐
│ STEP 6: API Call with Token                                   │
├───────────────────────────────────────────────────────────────┤
│ GET /api/resource                                             │
│ Authorization: Bearer ya29.a0...                              │
└───────────────────────────────────────────────────────────────┘

Common Scopes

Google:
  - openid                 → User ID
  - profile                → Name, photo
  - email                  → Email address
  - gmail.readonly         → Read emails
  - gmail.send             → Send emails
  - drive.file             → Access Drive files
  - calendar.readonly      → Read calendar

GitHub:
  - user                   → Profile info
  - repo                   → Repository access
  - admin:org              → Organization management
  - gist                   → Gist access

Facebook:
  - public_profile         → Public profile info
  - email                  → Email address
  - user_posts             → Read user posts
  - publish_pages          → Publish to pages

Token Lifetimes (Typical)

Authorization Code:  5-10 minutes    (single-use)
Access Token:        1 hour          (short-lived)
Refresh Token:       30-90 days      (long-lived, revocable)

Further Learning

Specifications

Interactive Tools

Common Providers


Summary

OAuth in One Sentence: OAuth lets users grant your app limited, revocable access to their data on another service, without sharing their password.

Key Takeaways:

  1. OAuth is for authorization (what you can do), not authentication (who you are)
  2. Access tokens are short-lived (1 hour), refresh tokens are long-lived (days/months)
  3. Authorization Code Flow for web apps with backend
  4. Authorization Code + PKCE for mobile and single-page apps
  5. Always use HTTPS, state parameter, and minimum scopes
  6. Never store tokens in localStorage or expose client_secret in frontend

The Big Picture: OAuth solves the problem of secure, limited, revocable access delegation. It's like giving someone a hotel key card with specific permissions and an expiration date, instead of giving them your master key.