OAuth 2.0 Flow Explained

Understanding modern authorization protocols for secure API access and social login

Introduction to OAuth 2.0

OAuth 2.0 is an industry-standard protocol for authorization that enables third-party applications to obtain limited access to a user's account on a server without exposing the user's credentials. In simpler terms, it allows users to share specific data with applications while keeping their usernames and passwords secret.

Real-World Analogy

Think of OAuth 2.0 as a valet key for your car. A valet key allows a parking attendant to park your car but prevents access to your trunk and glovebox. Similarly, OAuth 2.0 lets applications access specific parts of your account without giving them your full credentials.

For example, when you click "Sign in with Google" on a website:

  • You're not sharing your Google password with that website
  • You're granting the website specific permissions (like reading your profile info)
  • You can revoke this access later without changing your Google password
  • The website gets a secure "token" that represents your permissions

Core Concepts and Terminology

Term Description Example
Resource Owner The user who owns the data/resource You, when giving a site access to your Google account
Client The application requesting access A website using "Login with Google"
Authorization Server Verifies identity and issues tokens Google's OAuth server
Resource Server Server hosting the protected resources Google's API server holding your profile data
Access Token Credential used to access protected resources The token the website receives to access your data
Refresh Token Credential to obtain new access tokens Long-lived token to get new short-lived access tokens
Scope Permissions the client is requesting profile, email, calendar.readonly
Consent User's permission for the request The permission dialog shown by Google
graph TD subgraph "OAuth 2.0 Entities" A[Resource Owner] -->|gives permission to| B[Client] B -->|requests access token from| C[Authorization Server] C -->|issues token to| B B -->|accesses resources with token| D[Resource Server] end

OAuth 2.0 Grant Types

OAuth 2.0 defines several "grant types" or flows for different use cases. Each flow is designed for specific application types and security considerations:

Grant Type Use Case Security Level Best For
Authorization Code Web apps with server backend High Traditional web applications
Authorization Code with PKCE Mobile/native apps, SPAs High Mobile apps, single-page applications
Implicit Client-side apps (deprecated) Low Legacy applications (no longer recommended)
Resource Owner Password Credentials Trusted first-party apps Medium Legacy/migration scenarios
Client Credentials Service-to-service Medium-High Microservices, backend processes
Device Code Input-constrained devices Medium Smart TVs, IoT devices

Choosing the Right Flow

  • For web applications with a backend: Use Authorization Code flow
  • For mobile apps and SPAs: Use Authorization Code with PKCE
  • For service-to-service communication: Use Client Credentials flow
  • For smart TVs, printers, or IoT devices: Use Device Code flow
  • Never use Implicit flow for new applications
  • Avoid Resource Owner Password Credentials unless absolutely necessary

Authorization Code Flow in Detail

The Authorization Code flow is the most commonly used and most secure OAuth 2.0 flow. It's designed for applications that can securely store a client secret (typically server-side applications).

sequenceDiagram participant User as Resource Owner participant Browser participant Client as Client App (Server) participant Auth as Authorization Server participant API as Resource Server Note over User,API: 1. Authorization Request User->>Browser: Clicks "Login with Provider" Browser->>Client: Initiates login flow Client->>Browser: Redirects to Auth Server with client_id, redirect_uri, scope, state Browser->>Auth: GET /authorize with parameters Auth->>User: Displays login & consent screen User->>Auth: Authenticates & approves scopes Note over User,API: 2. Authorization Code Grant Auth->>Browser: Redirects to redirect_uri with code & state Browser->>Client: GET redirect_uri with code & state Note over User,API: 3. Token Exchange Client->>Auth: POST /token with code, client_id, client_secret, redirect_uri Auth->>Client: Returns access_token, refresh_token, expiry Note over User,API: 4. Resource Access Client->>API: Requests resource with access_token API->>Client: Returns protected resource Client->>Browser: Returns application data/view Browser->>User: Displays application

Step-by-Step Explanation

  1. Authorization Request:
    • The flow begins when the user clicks a "Login with X" button
    • The client application redirects to the authorization server
    • The redirect includes several parameters:
      • client_id: Application's registered ID
      • redirect_uri: Where to send the code
      • response_type=code: Indicates the auth code flow
      • scope: Requested permissions
      • state: Random value for CSRF protection
  2. User Authentication & Consent:
    • Authorization server authenticates the user (login screen)
    • Server presents a consent screen showing requested permissions
    • User approves or denies the permission request
  3. Authorization Code Grant:
    • Upon approval, server redirects back to the client's redirect_uri
    • The redirect includes an authorization code and the original state value
    • The code is short-lived (typically ~10 minutes)
  4. Token Exchange:
    • The client makes a server-to-server POST request to exchange the code for tokens
    • This request includes:
      • grant_type=authorization_code
      • code: The authorization code received
      • redirect_uri: Must match the original request
      • client_id: Application's ID
      • client_secret: Application's secret
    • If valid, the authorization server returns:
      • access_token: For API access
      • token_type: Usually "Bearer"
      • expires_in: Token lifetime in seconds
      • refresh_token: (Optional) For getting new access tokens
  5. Resource Access:
    • The client can now use the access token to request protected resources
    • Usually added as an HTTP header: Authorization: Bearer {token}
    • The resource server validates the token and responds with the requested data
// Example OAuth 2.0 Authorization Code Flow in Express.js

// Step 1: Redirect to authorization endpoint
app.get('/auth/login', (req, res) => {
  // Create and store a random state value for CSRF protection
  const state = crypto.randomBytes(16).toString('hex');
  req.session.oauthState = state;
  
  // Construct the authorization URL
  const authorizationUrl = new URL('https://authorization-server.com/oauth/authorize');
  authorizationUrl.searchParams.append('response_type', 'code');
  authorizationUrl.searchParams.append('client_id', process.env.OAUTH_CLIENT_ID);
  authorizationUrl.searchParams.append('redirect_uri', process.env.OAUTH_REDIRECT_URI);
  authorizationUrl.searchParams.append('scope', 'profile email');
  authorizationUrl.searchParams.append('state', state);
  
  // Redirect the user
  res.redirect(authorizationUrl.toString());
});

// Step 3: Handle the callback with authorization code
app.get('/auth/callback', async (req, res) => {
  // Extract the code and state from the query parameters
  const { code, state } = req.query;
  
  // Verify the state matches (CSRF protection)
  if (!state || state !== req.session.oauthState) {
    return res.status(403).send('Invalid state parameter');
  }
  
  // Clear the stored state
  delete req.session.oauthState;
  
  try {
    // Step 4: Exchange code for tokens
    const tokenResponse = await axios.post('https://authorization-server.com/oauth/token', {
      grant_type: 'authorization_code',
      code,
      redirect_uri: process.env.OAUTH_REDIRECT_URI,
      client_id: process.env.OAUTH_CLIENT_ID,
      client_secret: process.env.OAUTH_CLIENT_SECRET
    });
    
    // Extract tokens from the response
    const { access_token, refresh_token, expires_in } = tokenResponse.data;
    
    // Store tokens securely (e.g., in session or database)
    req.session.accessToken = access_token;
    // Store refresh token in a more persistent storage
    await saveRefreshToken(req.user.id, refresh_token);
    
    // Step 5: Use the access token to get user information
    const userInfoResponse = await axios.get('https://api.example.com/userinfo', {
      headers: {
        'Authorization': `Bearer ${access_token}`
      }
    });
    
    // Process user information
    const userInfo = userInfoResponse.data;
    
    // Create or update user in your database
    const user = await findOrCreateUser(userInfo);
    
    // Log the user in
    req.session.userId = user.id;
    
    // Redirect to the application
    res.redirect('/dashboard');
  } catch (error) {
    console.error('OAuth Error:', error);
    res.status(500).send('Authentication failed');
  }
});

Security Considerations for Authorization Code Flow

  • Always validate the state parameter to prevent CSRF attacks
  • Keep the client secret secure - never expose it in client-side code
  • Validate redirect URIs - only redirect to pre-registered URIs
  • Use HTTPS for all OAuth requests and redirects
  • Store tokens securely - access tokens in short-term storage (session), refresh tokens in secure long-term storage
  • Implement proper error handling for all OAuth-related requests

Authorization Code Flow with PKCE

Proof Key for Code Exchange (PKCE, pronounced "pixie") is an extension to the Authorization Code flow that provides additional security for public clients, such as mobile apps and single-page applications, which cannot securely store a client secret.

sequenceDiagram participant User as Resource Owner participant App as Client App (Public) participant Auth as Authorization Server participant API as Resource Server Note over User,API: 1. Code Verifier Generation App->>App: Generate random code_verifier App->>App: Create code_challenge from code_verifier (using S256) Note over User,API: 2. Authorization Request with Challenge App->>Auth: Authorization request with client_id, redirect_uri, code_challenge Auth->>User: Display login & consent screen User->>Auth: Authenticate & approve Note over User,API: 3. Authorization Code Grant Auth->>App: Redirect with authorization code Note over User,API: 4. Token Exchange with Verifier App->>Auth: Token request with code, client_id, code_verifier Auth->>Auth: Verify code_challenge matches code_verifier Auth->>App: Access token, refresh token Note over User,API: 5. Resource Access App->>API: Access protected resource with token API->>App: Protected resource

Key PKCE Concepts

PKCE Flow Steps

  1. Code Verifier Generation:
    • Client generates a cryptographically random code verifier (43-128 characters)
    • Client creates a code challenge by hashing the verifier (SHA-256) and base64-URL-encoding it
  2. Authorization Request:
    • The client includes additional parameters:
      • code_challenge: The transformed verifier
      • code_challenge_method=S256: The transformation method
  3. Token Exchange:
    • When exchanging the authorization code for tokens, the client includes:
      • code_verifier: The original verifier value
    • The authorization server transforms the verifier using the specified method and validates that it matches the original challenge
// Example of PKCE implementation in JavaScript

// 1. Generate a random code verifier
function generateCodeVerifier() {
  const array = new Uint8Array(32); // 32 bytes = 256 bits
  window.crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

// Base64Url encoding function
function base64UrlEncode(buffer) {
  return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

// 2. Create a code challenge from the verifier
async function generateCodeChallenge(codeVerifier) {
  // Hash the verifier with SHA-256
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  
  // Base64Url encode the hash
  return base64UrlEncode(digest);
}

// 3. Start the OAuth flow with PKCE
async function startOAuthFlow() {
  // Generate and store the code verifier
  const codeVerifier = generateCodeVerifier();
  localStorage.setItem('pkce_code_verifier', codeVerifier);
  
  // Generate the code challenge
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  
  // Generate random state
  const state = generateRandomString(16);
  localStorage.setItem('pkce_state', state);
  
  // Construct the authorization URL
  const authUrl = new URL('https://authorization-server.com/oauth/authorize');
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
  authUrl.searchParams.append('redirect_uri', 'https://your-app.com/callback');
  authUrl.searchParams.append('scope', 'profile email');
  authUrl.searchParams.append('state', state);
  authUrl.searchParams.append('code_challenge', codeChallenge);
  authUrl.searchParams.append('code_challenge_method', 'S256');
  
  // Redirect to the authorization server
  window.location = authUrl.toString();
}

// 4. Handle the callback and exchange code for tokens
async function handleCallback() {
  // Parse the query parameters
  const urlParams = new URLSearchParams(window.location.search);
  const code = urlParams.get('code');
  const state = urlParams.get('state');
  const storedState = localStorage.getItem('pkce_state');
  
  // Verify state parameter
  if (!state || state !== storedState) {
    throw new Error('Invalid state parameter');
  }
  
  // Get the stored code verifier
  const codeVerifier = localStorage.getItem('pkce_code_verifier');
  
  // Exchange the code for tokens
  const tokenResponse = await fetch('https://authorization-server.com/oauth/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code,
      redirect_uri: 'https://your-app.com/callback',
      client_id: 'YOUR_CLIENT_ID',
      code_verifier: codeVerifier
    })
  });
  
  // Process the token response
  const tokens = await tokenResponse.json();
  
  // Store the tokens (securely!)
  // ...
  
  // Clean up
  localStorage.removeItem('pkce_code_verifier');
  localStorage.removeItem('pkce_state');
  
  // Continue with the application flow
  // ...
}

When to Use PKCE

PKCE should be used in the following scenarios:

  • Mobile Applications: Where a client secret cannot be securely stored
  • Single-Page Applications (SPAs): Where all code runs in the browser
  • Native Desktop Applications: Where extracting a client secret is possible
  • IoT Applications: Where the client secret could be exposed

In fact, using PKCE is now considered a best practice for all OAuth 2.0 clients, even those that can securely store a client secret, as it provides an additional layer of security.

Client Credentials Flow

The Client Credentials flow is used for server-to-server authentication where no user is involved. It's perfect for microservices, backend jobs, and other scenarios where the application itself needs to access resources.

sequenceDiagram participant Client as Client Application participant Auth as Authorization Server participant API as Resource Server Note over Client,API: 1. Token Request Client->>Auth: POST /token with client_id, client_secret, grant_type=client_credentials Auth->>Client: Returns access_token, expires_in Note over Client,API: 2. Resource Access Client->>API: Request with access_token API->>Client: Protected resource

Client Credentials Implementation

// Example Node.js Client Credentials flow using Axios

const axios = require('axios');
const qs = require('querystring');

async function getServiceToken() {
  try {
    // Step 1: Request an access token
    const tokenResponse = await axios.post(
      'https://authorization-server.com/oauth/token',
      qs.stringify({
        grant_type: 'client_credentials',
        client_id: process.env.CLIENT_ID,
        client_secret: process.env.CLIENT_SECRET,
        scope: 'read:data write:data'
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded'
        }
      }
    );
    
    // Extract the access token
    const { access_token, expires_in } = tokenResponse.data;
    
    // Step 2: Use the token to access resources
    const apiResponse = await axios.get(
      'https://api.example.com/data',
      {
        headers: {
          'Authorization': `Bearer ${access_token}`
        }
      }
    );
    
    return apiResponse.data;
  } catch (error) {
    console.error('Error in Client Credentials flow:', error.message);
    throw error;
  }
}

// Usage in a microservice
async function synchronizeData() {
  try {
    const data = await getServiceToken();
    // Process the data...
    console.log(`Synchronized ${data.length} records`);
  } catch (error) {
    console.error('Synchronization failed:', error);
  }
}

// Schedule the job
setInterval(synchronizeData, 3600000); // Run every hour

Client Credentials Use Cases

  • Microservice Communication: When one service needs to call another service's API
  • Scheduled Jobs: For background processes that need API access
  • Data Synchronization: For server-side integrations with other systems
  • CI/CD Pipelines: For deployment scripts that need API access

Key difference: This flow doesn't involve a user, so there's no user consent or redirect flow.

Refresh Token Flow

Refresh tokens are an important part of OAuth 2.0 that allows clients to obtain new access tokens without requiring the user to re-authenticate. Access tokens are typically short-lived for security, while refresh tokens have a longer lifetime.

sequenceDiagram participant Client participant Auth as Authorization Server participant API as Resource Server Note over Client,API: 1. Initial Token Request (from Authorization Code or other flow) Auth->>Client: access_token, refresh_token, expires_in Note over Client,API: 2. Using Access Token Client->>API: Request with access_token API->>Client: Protected resource Note over Client,API: 3. Token Expiration Client->>API: Request with expired access_token API->>Client: 401 Unauthorized Note over Client,API: 4. Token Refresh Client->>Auth: POST /token with refresh_token, client_id, client_secret Auth->>Client: New access_token, (optionally new refresh_token), expires_in Note over Client,API: 5. Using New Access Token Client->>API: Request with new access_token API->>Client: Protected resource

Refresh Token Implementation

// Example refresh token implementation in Express.js

const axios = require('axios');
const { promisify } = require('util');

// Function to refresh an expired token
async function refreshAccessToken(refreshToken) {
  try {
    const response = await axios.post('https://authorization-server.com/oauth/token', {
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET
    });
    
    return {
      accessToken: response.data.access_token,
      refreshToken: response.data.refresh_token || refreshToken, // Some providers return a new refresh token
      expiresIn: response.data.expires_in
    };
  } catch (error) {
    console.error('Error refreshing token:', error.message);
    throw new Error('Failed to refresh access token');
  }
}

// Middleware to handle API requests with token refresh
async function apiRequest(req, res, next) {
  try {
    // Get user's tokens from the database or session
    const { accessToken, refreshToken, expiresAt } = await getUserTokens(req.user.id);
    
    // Check if the access token is expired
    const isExpired = Date.now() >= new Date(expiresAt).getTime();
    
    // If token is expired, refresh it
    let currentAccessToken = accessToken;
    if (isExpired) {
      try {
        const tokens = await refreshAccessToken(refreshToken);
        
        // Update tokens in the database
        await updateUserTokens(req.user.id, {
          accessToken: tokens.accessToken,
          refreshToken: tokens.refreshToken,
          expiresAt: new Date(Date.now() + tokens.expiresIn * 1000)
        });
        
        currentAccessToken = tokens.accessToken;
      } catch (refreshError) {
        // If refresh fails, user needs to re-authenticate
        return res.redirect('/auth/login');
      }
    }
    
    // Make the API request with the valid token
    try {
      const apiResponse = await axios.get(req.apiEndpoint, {
        headers: {
          Authorization: `Bearer ${currentAccessToken}`
        }
      });
      
      // Return the API response
      return res.json(apiResponse.data);
    } catch (apiError) {
      // If API returns 401 even after refresh, token might be revoked
      if (apiError.response && apiError.response.status === 401) {
        return res.redirect('/auth/login');
      }
      
      // Other API errors
      return res.status(apiError.response?.status || 500).json({
        error: 'API request failed',
        details: apiError.message
      });
    }
  } catch (error) {
    next(error);
  }
}

// Example usage
app.get('/api/user-profile', async (req, res, next) => {
  req.apiEndpoint = 'https://api.example.com/user/profile';
  return apiRequest(req, res, next);
});

Refresh Token Best Practices

  • Secure Storage: Store refresh tokens securely (e.g., in an encrypted database)
  • Token Rotation: Use new refresh tokens when they're provided
  • Expire Refresh Tokens: Even refresh tokens should have expiration (e.g., 30-90 days)
  • Manage Consent: Allow users to revoke access (invalidating refresh tokens)
  • Implement Refresh Logic: Automatically refresh when access tokens expire
  • Handle Refresh Failures: Redirect to re-authenticate if refresh fails

OAuth 2.0 Security Best Practices

General Security Considerations

Common Security Vulnerabilities

Vulnerability Description Mitigation
CSRF Attacks Forging authorization requests Use the state parameter, SameSite cookies
Token Leakage Exposing tokens in logs, URLs, etc. Only send tokens in headers or POST body
Open Redirectors Using unvalidated redirect_uri parameters Whitelist valid redirect URIs
Authorization Code Theft Stealing codes from insecure clients Use PKCE, short code lifetimes, one-time use
Token Replay Stolen tokens being reused Short token lifetimes, token binding
Phishing Fake authorization servers Use trusted providers, educate users

Critical Security Updates

The OAuth 2.0 security landscape has evolved. Some key updates:

  • Implicit flow is deprecated - Authorization Code with PKCE is the recommended replacement
  • PKCE is recommended for all clients, not just public clients
  • Additional security headers like Content-Security-Policy are important
  • OAuth 2.1 is being developed to consolidate best practices

Implementing Social Login with OAuth 2.0

Social login (or "Login with X") is one of the most common implementations of OAuth 2.0. Let's look at how to implement it in a Node.js application.

General Implementation Steps

  1. Register your application with the OAuth provider
  2. Implement the OAuth flow in your application
  3. Extract user profile information from the provider's API
  4. Create or update user records in your application
  5. Establish a session for the authenticated user
// Example social login implementation with Google OAuth

// 1. Install required packages
// npm install express express-session passport passport-google-oauth20

const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

const app = express();

// 2. Configure session middleware
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// 3. Initialize Passport
app.use(passport.initialize());
app.use(passport.session());

// 4. Configure Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/auth/google/callback',
    scope: ['profile', 'email']
  },
  function(accessToken, refreshToken, profile, done) {
    // Find or create user in your database
    findOrCreateUser({ 
      googleId: profile.id,
      email: profile.emails[0].value,
      name: profile.displayName,
      picture: profile.photos[0].value
    })
      .then(user => done(null, user))
      .catch(err => done(err));
  }
));

// 5. Serialize/deserialize user for sessions
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  findUserById(id)
    .then(user => done(null, user))
    .catch(err => done(err));
});

// 6. Set up routes for authentication
// Start OAuth flow - redirect to Google
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

// OAuth callback route
app.get('/auth/google/callback', 
  passport.authenticate('google', { 
    failureRedirect: '/login',
    successRedirect: '/dashboard'
  })
);

// Logout route
app.get('/logout', (req, res) => {
  req.logout();
  res.redirect('/');
});

// 7. Protected route example
app.get('/dashboard', ensureAuthenticated, (req, res) => {
  res.render('dashboard', { user: req.user });
});

// Middleware to check if user is authenticated
function ensureAuthenticated(req, res, next) {
  if (req.isAuthenticated()) {
    return next();
  }
  res.redirect('/login');
}

// 8. Database helper functions (implementation depends on your database)
async function findOrCreateUser(profile) {
  // Implementation depends on your database
  // Example with Mongoose:
  let user = await User.findOne({ googleId: profile.googleId });
  
  if (!user) {
    user = await User.create({
      googleId: profile.googleId,
      email: profile.email,
      name: profile.name,
      picture: profile.picture
    });
  }
  
  return user;
}

async function findUserById(id) {
  // Example with Mongoose:
  return await User.findById(id);
}

// Start the server
app.listen(3000, () => {
  console.log('Server started on http://localhost:3000');
});

Multi-Provider Social Login

// Adding multiple OAuth providers

// 1. Install additional strategies
// npm install passport-facebook passport-github2 passport-twitter

const FacebookStrategy = require('passport-facebook').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;
const TwitterStrategy = require('passport-twitter').Strategy;

// 2. Configure Facebook Strategy
passport.use(new FacebookStrategy({
    clientID: process.env.FACEBOOK_APP_ID,
    clientSecret: process.env.FACEBOOK_APP_SECRET,
    callbackURL: 'http://localhost:3000/auth/facebook/callback',
    profileFields: ['id', 'emails', 'name', 'picture.type(large)']
  },
  function(accessToken, refreshToken, profile, done) {
    findOrCreateUser({
      facebookId: profile.id,
      email: profile.emails?.[0]?.value,
      name: `${profile.name.givenName} ${profile.name.familyName}`,
      picture: profile.photos?.[0]?.value
    })
      .then(user => done(null, user))
      .catch(err => done(err));
  }
));

// 3. Configure GitHub Strategy
passport.use(new GitHubStrategy({
    clientID: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/auth/github/callback'
  },
  function(accessToken, refreshToken, profile, done) {
    findOrCreateUser({
      githubId: profile.id,
      email: profile.emails?.[0]?.value,
      name: profile.displayName || profile.username,
      picture: profile.photos?.[0]?.value
    })
      .then(user => done(null, user))
      .catch(err => done(err));
  }
));

// 4. Add routes for each provider
app.get('/auth/facebook',
  passport.authenticate('facebook', { scope: ['email'] })
);

app.get('/auth/facebook/callback',
  passport.authenticate('facebook', { 
    failureRedirect: '/login',
    successRedirect: '/dashboard'
  })
);

app.get('/auth/github',
  passport.authenticate('github', { scope: ['user:email'] })
);

app.get('/auth/github/callback',
  passport.authenticate('github', { 
    failureRedirect: '/login',
    successRedirect: '/dashboard'
  })
);

// 5. Update your database schema to handle multiple provider IDs
// Example Mongoose schema:
const userSchema = new mongoose.Schema({
  email: { type: String, unique: true },
  name: String,
  picture: String,
  googleId: String,
  facebookId: String,
  githubId: String,
  // Add more provider IDs as needed
});

// 6. Update findOrCreateUser to handle multiple providers
async function findOrCreateUser(profile) {
  // First try to find by provider ID
  let user = null;
  
  if (profile.googleId) {
    user = await User.findOne({ googleId: profile.googleId });
  } else if (profile.facebookId) {
    user = await User.findOne({ facebookId: profile.facebookId });
  } else if (profile.githubId) {
    user = await User.findOne({ githubId: profile.githubId });
  }
  
  // If not found by provider ID but email exists, try to find by email
  if (!user && profile.email) {
    user = await User.findOne({ email: profile.email });
    
    // If user exists with this email, link the new provider ID
    if (user) {
      if (profile.googleId) user.googleId = profile.googleId;
      if (profile.facebookId) user.facebookId = profile.facebookId;
      if (profile.githubId) user.githubId = profile.githubId;
      
      await user.save();
    }
  }
  
  // If user still not found, create a new one
  if (!user) {
    user = await User.create(profile);
  }
  
  return user;
}

Challenges with Social Login

  • Account Linking: Users might sign up with different providers using the same email
  • Data Consistency: Different providers return different profile information
  • Provider Changes: Social providers might change their APIs or policies
  • Dependency Risk: Your auth system depends on third-party services
  • Account Recovery: Need alternative methods if the social account is lost

Best practice: Always offer a traditional email/password option alongside social login.

OAuth 2.0 in Single Page Applications (SPAs)

Implementing OAuth 2.0 in SPAs presents unique challenges since the client cannot securely store secrets.

SPA Authentication Approaches

Approach Description Pros Cons
Backend For Frontend (BFF) Backend proxy that handles OAuth flow and sets session cookies High security, no client storage of tokens Requires additional server component
Auth Code + PKCE SPA uses Authorization Code flow with PKCE Standard approach, no backend required Requires secure token storage in browser
Implicit Flow Deprecated approach that returns token in URL fragment Simple implementation Security vulnerabilities, not recommended

Recommended: Auth Code + PKCE for SPAs

// Example OAuth implementation in a React application

// 1. Install required packages
// npm install react-router-dom axios

import React, { useState, useEffect, createContext, useContext } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';

// Create an auth context
const AuthContext = createContext(null);

// Helper functions for PKCE
function generateCodeVerifier() {
  const array = new Uint8Array(32);
  window.crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

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

async function generateCodeChallenge(codeVerifier) {
  const encoder = new TextEncoder();
  const data = encoder.encode(codeVerifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  return base64UrlEncode(digest);
}

// Auth Provider component
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  const navigate = useNavigate();
  const location = useLocation();
  
  // Check if we're returning from an OAuth redirect
  useEffect(() => {
    async function handleRedirect() {
      // Check if this is a redirect from OAuth provider
      if (location.pathname === '/callback') {
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const state = urlParams.get('state');
        
        // Validate state
        const storedState = localStorage.getItem('oauth_state');
        if (!state || state !== storedState) {
          setError('Invalid state parameter');
          setLoading(false);
          return;
        }
        
        // Get code verifier
        const codeVerifier = localStorage.getItem('code_verifier');
        if (!codeVerifier) {
          setError('No code verifier found');
          setLoading(false);
          return;
        }
        
        try {
          // Exchange code for tokens
          const response = await axios.post('https://authorization-server.com/oauth/token', {
            grant_type: 'authorization_code',
            code,
            redirect_uri: window.location.origin + '/callback',
            client_id: 'YOUR_CLIENT_ID',
            code_verifier: codeVerifier
          });
          
          // Store tokens securely (localStorage is not ideal but common)
          localStorage.setItem('access_token', response.data.access_token);
          localStorage.setItem('refresh_token', response.data.refresh_token);
          localStorage.setItem('token_expiry', Date.now() + response.data.expires_in * 1000);
          
          // Clean up PKCE and state parameters
          localStorage.removeItem('oauth_state');
          localStorage.removeItem('code_verifier');
          
          // Fetch user info
          await fetchUserInfo();
          
          // Redirect to the page they were trying to access
          const intendedPath = localStorage.getItem('auth_redirect') || '/dashboard';
          localStorage.removeItem('auth_redirect');
          navigate(intendedPath);
        } catch (err) {
          console.error('Authentication error:', err);
          setError('Failed to authenticate');
          setLoading(false);
        }
      } else {
        // Not a redirect, check if we're already logged in
        const token = localStorage.getItem('access_token');
        if (token) {
          try {
            await fetchUserInfo();
          } catch (err) {
            // Token might be invalid, try refresh token or clear
            try {
              await refreshTokens();
              await fetchUserInfo();
            } catch (refreshErr) {
              logout();
            }
          }
        }
        setLoading(false);
      }
    }
    
    handleRedirect();
  }, [location.pathname]);
  
  // Fetch user info
  async function fetchUserInfo() {
    try {
      const token = localStorage.getItem('access_token');
      const response = await axios.get('https://api.example.com/userinfo', {
        headers: {
          Authorization: `Bearer ${token}`
        }
      });
      
      setUser(response.data);
      setLoading(false);
      return response.data;
    } catch (err) {
      throw err;
    }
  }
  
  // Refresh tokens
  async function refreshTokens() {
    try {
      const refreshToken = localStorage.getItem('refresh_token');
      if (!refreshToken) throw new Error('No refresh token');
      
      const response = await axios.post('https://authorization-server.com/oauth/token', {
        grant_type: 'refresh_token',
        refresh_token: refreshToken,
        client_id: 'YOUR_CLIENT_ID'
      });
      
      localStorage.setItem('access_token', response.data.access_token);
      if (response.data.refresh_token) {
        localStorage.setItem('refresh_token', response.data.refresh_token);
      }
      localStorage.setItem('token_expiry', Date.now() + response.data.expires_in * 1000);
      return response.data;
    } catch (err) {
      logout();
      throw err;
    }
  }
  
  // Start login flow
  async function login() {
    try {
      // Generate and store PKCE values
      const codeVerifier = generateCodeVerifier();
      const codeChallenge = await generateCodeChallenge(codeVerifier);
      
      // Generate and store state
      const state = base64UrlEncode(window.crypto.getRandomValues(new Uint8Array(16)));
      
      // Store values in localStorage
      localStorage.setItem('code_verifier', codeVerifier);
      localStorage.setItem('oauth_state', state);
      
      // Store the current path for redirect after login
      localStorage.setItem('auth_redirect', location.pathname);
      
      // Redirect to authorization server
      const authUrl = new URL('https://authorization-server.com/oauth/authorize');
      authUrl.searchParams.append('response_type', 'code');
      authUrl.searchParams.append('client_id', 'YOUR_CLIENT_ID');
      authUrl.searchParams.append('redirect_uri', window.location.origin + '/callback');
      authUrl.searchParams.append('scope', 'profile email');
      authUrl.searchParams.append('state', state);
      authUrl.searchParams.append('code_challenge', codeChallenge);
      authUrl.searchParams.append('code_challenge_method', 'S256');
      
      window.location.href = authUrl.toString();
    } catch (err) {
      console.error('Login error:', err);
      setError('Failed to start login flow');
    }
  }
  
  // Logout
  function logout() {
    localStorage.removeItem('access_token');
    localStorage.removeItem('refresh_token');
    localStorage.removeItem('token_expiry');
    setUser(null);
    navigate('/');
  }
  
  // Create authenticated HTTP client
  function createAuthenticatedClient() {
    const client = axios.create();
    
    // Add interceptor to handle token expiration
    client.interceptors.request.use(async (config) => {
      let token = localStorage.getItem('access_token');
      const expiry = localStorage.getItem('token_expiry');
      
      // Check if token is expired or will expire soon
      if (expiry && Date.now() > parseInt(expiry) - 60000) {
        // Token expired or will expire soon, refresh it
        try {
          const tokens = await refreshTokens();
          token = tokens.access_token;
        } catch (err) {
          // Refresh failed, redirect to login
          logout();
          throw err;
        }
      }
      
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      
      return config;
    });
    
    return client;
  }
  
  // Create the auth value
  const value = {
    user,
    loading,
    error,
    login,
    logout,
    createAuthenticatedClient
  };
  
  return (
    
      {children}
    
  );
}

// Custom hook to use the auth context
export function useAuth() {
  return useContext(AuthContext);
}

// Example component usage
function LoginButton() {
  const { login } = useAuth();
  
  return (
    
  );
}

function UserProfile() {
  const { user, loading, error, createAuthenticatedClient } = useAuth();
  const [profile, setProfile] = useState(null);
  
  useEffect(() => {
    async function fetchProfile() {
      if (!user) return;
      
      const client = createAuthenticatedClient();
      const response = await client.get('https://api.example.com/profile');
      setProfile(response.data);
    }
    
    fetchProfile();
  }, [user]);
  
  if (loading) return 
Loading...
; if (error) return
Error: {error}
; if (!user) return
Please log in
; return (

Profile: {user.name}

{profile && (
{/* Display profile data */}
)}
); }

SPA Security Considerations

  • Token Storage: Browser storage has security limitations
  • XSS Risks: JavaScript access to tokens creates vulnerability
  • Token Expiration: Use short-lived access tokens and refresh mechanisms
  • Alternative: BFF Pattern: For higher security, consider a Backend For Frontend approach

Practice Activities

Activity 1: Implement Google OAuth Login

Create a simple Express.js application with Google OAuth login:

  1. Register a new application in the Google Developer Console
  2. Configure OAuth consent screen and create OAuth credentials
  3. Implement the Authorization Code flow in your Express.js app
  4. Use Passport.js or implement the flow directly using axios
  5. Store user information in a database (MongoDB or similar)
  6. Create protected routes that require authentication

Bonus: Add a "Login with GitHub" option as well.

Activity 2: Create an OAuth 2.0 Client-Credentials Flow

Build a service-to-service authentication system:

  1. Create a simple authorization server with Express.js
  2. Implement the client credentials grant type
  3. Create a resource server that validates tokens
  4. Implement a client application that uses the client credentials flow
  5. Add token expiration and refresh mechanisms
  6. Test with different client credentials

Activity 3: Implement OAuth in a React SPA

Create a single-page application with OAuth authentication:

  1. Set up a React application with React Router
  2. Implement the Authorization Code flow with PKCE
  3. Create an authentication context for state management
  4. Implement protected routes that require authentication
  5. Add token refresh handling
  6. Create a user profile page showing data from the OAuth provider

Activity 4: Build an OAuth Authorization Server

Create your own basic OAuth 2.0 authorization server:

  1. Implement client registration and management
  2. Create authorization endpoint for the authorization code flow
  3. Implement token endpoint for issuing access and refresh tokens
  4. Add support for different scopes
  5. Implement token validation
  6. Create a simple client application that uses your authorization server

Note: This is an advanced activity and should be approached incrementally.

Additional Resources

Summary

Recent Developments in OAuth 2.0

OAuth 2.0 continues to evolve with new extensions and improvements. Here are some of the latest developments:

OAuth 2.1

OAuth 2.1 is not a complete rewrite but a consolidation of the best practices and security improvements that have emerged since OAuth 2.0 was published. Key changes include:

DPoP (Demonstrating Proof of Possession)

DPoP is an extension that adds a mechanism for preventing token theft. It works by:

Resource Indicators

This extension allows clients to specify which resource server they want to access when requesting tokens, enabling more granular access control and improved token handling for multi-resource scenarios.

JWT Secured Authorization Response Mode (JARM)

JARM adds an extra layer of security by returning authorization responses as signed JWTs, protecting against response tampering and information leakage.

Beyond OAuth: Related Protocols

OAuth 2.0 is often used in conjunction with other protocols to provide a complete identity and access management solution:

OpenID Connect (OIDC)

OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth 2.0 is about authorization (what you can do), OpenID Connect is about authentication (who you are).

Feature OAuth 2.0 OpenID Connect
Primary Purpose Authorization Authentication
Core Concept Access tokens ID tokens (JWT)
User Info No standardized way Standardized claims and userinfo endpoint
Discovery Not defined Well-defined discovery mechanism
Client Registration Implementation-specific Standardized dynamic registration

OpenID Connect Flow

The OIDC flow typically works like this:

  1. Client initiates authentication with an OpenID provider
  2. User authenticates and consents to share information
  3. Provider returns an ID token (JWT) containing identity information
  4. Client validates the ID token
  5. Optionally, client may use the access token to get more user info

The ID token contains standardized claims about the user, like name, email, and profile picture.

User-Managed Access (UMA)

UMA is an OAuth-based protocol that enables resource owners to control access to their resources across different services, defining detailed authorization policies.

Financial-grade API (FAPI)

FAPI is a security profile of OAuth 2.0 specifically designed for financial services and other high-value services, adding extra security requirements to mitigate risks specific to those domains.

Common OAuth Implementation Scenarios

Let's explore some common real-world OAuth implementation scenarios and best practices:

Mobile Backend for Frontend (Mobile BFF)

For native mobile apps, a common pattern is to create a dedicated backend for the mobile client that handles OAuth flows:

Microservices Architecture

In a microservices environment, OAuth can be implemented in several ways:

graph TD subgraph "Microservices OAuth Architecture" Client[Client Application] AuthService[Auth Service] Gateway[API Gateway] ServiceA[Service A] ServiceB[Service B] ServiceC[Service C] Client -->|1. User authenticates| AuthService AuthService -->|2. Returns tokens| Client Client -->|3. API call with token| Gateway Gateway -->|4. Validate token| AuthService Gateway -->|5. Forward request with user context| ServiceA ServiceA -->|6. Service-to-service call| ServiceB ServiceB -->|7. Call with client credentials| ServiceC end

Multi-Tenant SaaS Applications

SaaS applications that serve multiple customers often use OAuth for customer-specific authentication:

Debugging OAuth Flows

Debugging OAuth flows can be challenging due to their distributed nature. Here are some strategies and tools:

Common OAuth Debug Points

Debugging Tools

Useful OAuth Debugging Tools

  • Browser Dev Tools: Network tab for HTTP requests/responses
  • OAuth Debugger: Tools like oauthdebugger.com
  • JWT Debugger: Decode and verify tokens at jwt.io
  • Postman: Test OAuth flows and API requests
  • Charles Proxy/Fiddler: Intercept and analyze HTTP traffic
  • OAuth Provider Logs: Check logs in your authorization server

Common OAuth Errors and Solutions

Error Possible Causes Solutions
invalid_request Missing or invalid parameter Check all required parameters and formats
invalid_client Client authentication failed Verify client_id and client_secret
invalid_grant Authorization code invalid or expired Check code expiration, ensure one-time use
unauthorized_client Client not authorized for this grant type Check client configuration in OAuth provider
invalid_scope Requested scope is invalid or unknown Verify requested scopes are available to your client
redirect_uri_mismatch Redirect URI doesn't match registered URI Ensure exact match including protocol, path, and case
access_denied User denied the request Improve consent screen, request fewer permissions

OAuth in Production

Moving an OAuth implementation to production requires additional considerations:

Production Checklist

Monitoring and Alerting

Key metrics to monitor in your OAuth implementation:

Setting Up OAuth in AWS Environment

Example architecture for a secure OAuth implementation in AWS:

  • Use AWS Cognito or Auth0 as your OAuth provider
  • Store client secrets in AWS Secrets Manager
  • Place OAuth endpoints behind AWS WAF for protection
  • Implement AWS CloudWatch alerts for security events
  • Use AWS Lambda for token validation and processing
  • Consider AWS API Gateway for API protection

Conclusion

OAuth 2.0 has become the industry standard for authorization, enabling secure access to resources across different services. By understanding the core flows and security considerations, you can implement robust authentication and authorization in your applications.

Key takeaways:

As we move forward in our authentication journey, we'll explore how to integrate OAuth with specific providers and implement more advanced authentication patterns.

Homework Assignment

To solidify your understanding of OAuth 2.0, complete the following assignment:

OAuth Provider Comparison

  1. Choose three popular OAuth providers (e.g., Google, GitHub, Auth0, Okta)
  2. Create developer accounts and register a test application with each
  3. Document the setup process, required settings, and available options
  4. Compare the scopes/permissions available from each provider
  5. Implement a basic authentication flow with each provider
  6. Test the implementations and document any differences
  7. Create a comparison table highlighting the pros and cons of each provider
  8. Present your findings in a short report or presentation

This assignment will give you hands-on experience with real OAuth providers and help you understand the practical considerations when implementing OAuth in production applications.

Next Class: Passport.js Strategies

In our next session, we'll explore Passport.js, a popular authentication middleware for Node.js that makes implementing various OAuth strategies simpler. We'll cover:

Come prepared with your OAuth knowledge, as we'll build on the concepts we've covered today.