๐Ÿ”ฅ 0
โญ 0
Lesson 2 of 6 25 min +250 XP

OAuth 2.0 Flows

Not all applications are the same. A web app running on a server has different security capabilities than a mobile app or a command-line tool. OAuth 2.0 provides different flows (also called grant types) for different scenarios.

The Main OAuth 2.0 Flows

๐ŸŒ
Authorization Code

Web apps with backend

๐Ÿ“ฑ
Auth Code + PKCE

Mobile apps, SPAs

๐Ÿ–ฅ๏ธ
Client Credentials

Server-to-server

โš ๏ธ
Implicit (Deprecated)

Legacy SPAs

Authorization Code Flow

The most secure and commonly used flow for web applications with a backend server.

How It Works

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   User   โ”‚     โ”‚  Your App     โ”‚     โ”‚  Auth Server    โ”‚
โ”‚ (Browser)โ”‚     โ”‚  (Backend)    โ”‚     โ”‚  (e.g. Google)  โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 1. Click "Login"  โ”‚                      โ”‚
     โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚                      โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 2. Redirect to auth server              โ”‚
     โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚                      โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 3. Login + Consentโ”‚                      โ”‚
     โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 4. Redirect with authorization code     โ”‚
     โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 5. Send code to backend                 โ”‚
     โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚                      โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚                   โ”‚ 6. Exchange code for tokens
     โ”‚                   โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚                   โ”‚ 7. Access + Refresh tokens
     โ”‚                   โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
     โ”‚                   โ”‚                      โ”‚
     โ”‚ 8. User logged in โ”‚                      โ”‚
     โ”‚<โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚                      โ”‚

Step-by-Step Code Example

Step 1: Redirect user to authorization server
// Your app generates the authorization URL
const authUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth');
authUrl.searchParams.set('client_id', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid email profile');
authUrl.searchParams.set('state', generateRandomState()); // CSRF protection

// Redirect user
window.location.href = authUrl.toString();
Step 2: Handle the callback
// User is redirected back to https://yourapp.com/callback?code=ABC123&state=XYZ
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;

  // Verify state to prevent CSRF
  if (state !== req.session.oauthState) {
    return res.status(403).send('Invalid state');
  }

  // Exchange code for tokens
  const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: 'YOUR_CLIENT_ID',
      client_secret: 'YOUR_CLIENT_SECRET', // Kept safe on server!
      code: code,
      grant_type: 'authorization_code',
      redirect_uri: 'https://yourapp.com/callback'
    })
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, expires_in, id_token }
});
Why Authorization Code is Secure
  • Client secret never exposed to browser
  • Tokens exchanged on secure backend channel
  • Authorization code is short-lived and single-use
  • State parameter prevents CSRF attacks

Authorization Code with PKCE

PKCE (Proof Key for Code Exchange, pronounced "pixy") adds security for apps that can't safely store a client secret - like mobile apps and single-page applications.

The Problem PKCE Solves

Mobile apps can be decompiled. SPAs run entirely in the browser. The client secret isn't secret anymore!

How PKCE Works

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  1. App generates:                                        โ”‚
โ”‚     - code_verifier: random 43-128 character string       โ”‚
โ”‚     - code_challenge: SHA256(code_verifier), base64url    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                            โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  2. App sends code_challenge with auth request            โ”‚
โ”‚     (keeps code_verifier secret)                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                            โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  3. Auth server stores code_challenge with auth code      โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                            โ”‚
                            โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  4. App exchanges code + code_verifier for tokens         โ”‚
โ”‚     Auth server verifies: SHA256(verifier) == challenge   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

PKCE Code Example

// Step 1: Generate PKCE values
function generatePKCE() {
  // Generate random code_verifier
  const verifier = base64URLEncode(crypto.getRandomValues(new Uint8Array(32)));

  // Create code_challenge from verifier
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const challenge = base64URLEncode(new Uint8Array(hash));

  return { verifier, challenge };
}

// Step 2: Authorization request with challenge
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', 'YOUR_CLIENT_ID');
authUrl.searchParams.set('redirect_uri', 'https://yourapp.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');

// Step 3: Token exchange with verifier
const verifier = sessionStorage.getItem('pkce_verifier');
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    client_id: 'YOUR_CLIENT_ID',
    code: authorizationCode,
    grant_type: 'authorization_code',
    redirect_uri: 'https://yourapp.com/callback',
    code_verifier: verifier  // Proves we started this flow
  })
});
PKCE Security

Even if an attacker intercepts the authorization code, they can't exchange it for tokens without the code_verifier. And they can't compute the verifier from the challenge (SHA-256 is one-way).

Client Credentials Flow

For machine-to-machine communication where no user is involved.

Use Cases

  • Backend service calling another API
  • Scheduled jobs accessing resources
  • Microservices communicating internally

How It Works

// Simple token request - no user interaction needed
const tokenResponse = await fetch('https://api.stripe.com/oauth/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Authorization': 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET)
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'read_only'
  })
});

const { access_token } = await tokenResponse.json();

// Use token to call API
const data = await fetch('https://api.stripe.com/v1/customers', {
  headers: { 'Authorization': `Bearer ${access_token}` }
});

When to Use

  • Server-to-server APIs
  • Background jobs
  • Service accounts

Never Use For

  • User-facing applications
  • Mobile apps
  • Browser-based apps

Choosing the Right Flow

Application Type Recommended Flow Why
Web app (with backend) Authorization Code Can securely store client secret
Single-page app (SPA) Auth Code + PKCE No backend, can't store secrets
Mobile app Auth Code + PKCE Can be decompiled, secrets exposed
Server-to-server Client Credentials No user involved, trusted environment
CLI tool Device Code or Auth Code + PKCE Limited input capabilities

Real-World: GitHub OAuth

GitHub uses Authorization Code flow. Here's how CircleCI integrates:

// 1. CircleCI redirects you to GitHub
// https://github.com/login/oauth/authorize?
//   client_id=CIRCLECI_CLIENT_ID&
//   redirect_uri=https://circleci.com/auth/github&
//   scope=repo,user:email&
//   state=random123

// 2. After you approve, GitHub redirects back:
// https://circleci.com/auth/github?code=ABC123&state=random123

// 3. CircleCI exchanges code for token (server-side)
const response = await fetch('https://github.com/login/oauth/access_token', {
  method: 'POST',
  headers: { 'Accept': 'application/json' },
  body: JSON.stringify({
    client_id: GITHUB_CLIENT_ID,
    client_secret: GITHUB_CLIENT_SECRET,
    code: 'ABC123'
  })
});

// 4. CircleCI can now access your repos via GitHub API
const repos = await fetch('https://api.github.com/user/repos', {
  headers: { 'Authorization': `token ${access_token}` }
});

Key Takeaways

  • Authorization Code - Best for web apps with backend (can store secrets)
  • Auth Code + PKCE - Required for SPAs and mobile (can't store secrets)
  • Client Credentials - Machine-to-machine only (no user involved)
  • Implicit is deprecated - Always use PKCE for browser apps

Next up: Understanding Tokens - Access tokens, refresh tokens, and how they work together.

๐Ÿง  Quick Quiz

Test your understanding of this lesson.

1

Which OAuth flow is recommended for single-page applications?

2

What does PKCE protect against?

3

When should you use the Client Credentials flow?

Authentication vs Authorization