Skip to main content
โšก Calmops

OAuth Integration for Websites: Practical Implementation Guide

Introduction

Adding OAuth login to your website allows users to authenticate using existing accounts from Google, GitHub, Facebook, or other identity providers. This guide provides practical code examples for implementing OAuth 2.0 authentication in web applications, from basic setup to production-ready implementations.

OAuth Flow Overview

Authorization Code Flow with PKCE

For web applications, the recommended flow is Authorization Code Flow with PKCE (Proof Key for Code Exchange):

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  OAuth 2.0 Authorization Code + PKCE              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                                  โ”‚
โ”‚  1. User clicks "Login with Google"                               โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  2. Generate code_verifier (random string)                        โ”‚
โ”‚     Generate code_challenge = sha256(code_verifier)              โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  3. Redirect to Google OAuth:                                    โ”‚
โ”‚     https://accounts.google.com/o/oauth2/v2/auth?               โ”‚
โ”‚       client_id=YOUR_CLIENT_ID                                   โ”‚
โ”‚       redirect_uri=YOUR_CALLBACK_URL                             โ”‚
โ”‚       response_type=code                                         โ”‚
โ”‚       scope=openid email profile                                โ”‚
โ”‚       code_challenge=XXX                                         โ”‚
โ”‚       code_challenge_method=S256                                 โ”‚
โ”‚       state=random_string                                        โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  4. User logs in at Google, authorizes your app                   โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  5. Google redirects to your callback with:                      โ”‚
โ”‚     ?code=AUTH_CODE&state=random_string                          โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  6. Your server exchanges code for tokens:                       โ”‚
โ”‚     POST https://oauth2.googleapis.com/token                     โ”‚
โ”‚       code=AUTH_CODE                                            โ”‚
โ”‚       client_id=YOUR_CLIENT_ID                                    โ”‚
โ”‚       code_verifier=code_verifier                               โ”‚
โ”‚       grant_type=authorization_code                              โ”‚
โ”‚         โ”‚                                                        โ”‚
โ”‚         โ–ผ                                                        โ”‚
โ”‚  7. Google returns:                                              โ”‚
โ”‚     { access_token, refresh_token, id_token, expires_in }         โ”‚
โ”‚                                                                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Server-Side Implementation

Node.js/Express Implementation

// server.js
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');
const session = require('express-session');

const app = express();

// Configuration
const config = {
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    redirectUri: 'http://localhost:3000/auth/google/callback',
    authorizationUrl: 'https://oauth2.googleapis.com/oauth2/v4/authorize',
    tokenUrl: 'https://oauth2.googleapis.com/token',
    userInfoUrl: 'https://www.googleapis.com/oauth2/v3/userinfo'
  },
  github: {
    clientId: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    redirectUri: 'http://localhost:3000/auth/github/callback',
    authorizationUrl: 'https://github.com/login/oauth/authorize',
    tokenUrl: 'https://github.com/login/oauth/access_token',
    userInfoUrl: 'https://api.github.com/user'
  }
};

// Session setup
app.use(session({
  secret: process.env.SESSION_SECRET || 'dev-secret-change-in-production',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Generate PKCE code verifier and challenge
function generatePKCE() {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
  
  return { codeVerifier, codeChallenge };
}

// Generate random state
function generateState() {
  return crypto.randomBytes(16).toString('hex');
}

// Login with Google
app.get('/auth/google', (req, res) => {
  const { codeVerifier, codeChallenge } = generatePKCE();
  const state = generateState();
  
  // Store in session for verification
  req.session.oauthState = state;
  req.session.codeVerifier = codeVerifier;
  req.session.provider = 'google';
  
  const params = new URLSearchParams({
    client_id: config.google.clientId,
    redirect_uri: config.google.redirectUri,
    response_type: 'code',
    scope: 'openid email profile',
    code_challenge: codeChallenge,
    code_challenge_method: 'S256',
    state: state
  });
  
  res.redirect(`${config.google.authorizationUrl}?${params}`);
});

// Login with GitHub
app.get('/auth/github', (req, res) => {
  const state = generateState();
  req.session.oauthState = state;
  req.session.provider = 'github';
  
  const params = new URLSearchParams({
    client_id: config.github.clientId,
    redirect_uri: config.github.redirectUri,
    scope: 'read:user user:email',
    state: state
  });
  
  res.redirect(`${config.github.authorizationUrl}?${params}`);
});

// Google callback
app.get('/auth/google/callback', async (req, res) => {
  try {
    const { code, state } = req.query;
    
    // Verify state
    if (state !== req.session.oauthState) {
      return res.status(400).send('Invalid state parameter');
    }
    
    // Exchange code for tokens
    const tokenResponse = await axios.post(config.google.tokenUrl, {
      code: code,
      client_id: config.google.clientId,
      client_secret: config.google.clientSecret,
      code_verifier: req.session.codeVerifier,
      grant_type: 'authorization_code',
      redirect_uri: config.google.redirectUri
    });
    
    const { access_token, id_token, refresh_token, expires_in } = tokenResponse.data;
    
    // Get user info
    const userInfoResponse = await axios.get(config.google.userInfoUrl, {
      headers: { Authorization: `Bearer ${access_token}` }
    });
    
    const user = userInfoResponse.data;
    
    // Create session
    req.session.user = {
      id: user.sub,
      email: user.email,
      name: user.name,
      picture: user.picture,
      provider: 'google'
    };
    req.session.accessToken = access_token;
    req.session.refreshToken = refresh_token;
    
    // Clean up temp session data
    delete req.session.oauthState;
    delete req.session.codeVerifier;
    
    res.redirect('/dashboard');
    
  } catch (error) {
    console.error('OAuth error:', error.response?.data || error.message);
    res.status(500).send('Authentication failed');
  }
});

// GitHub callback
app.get('/auth/github/callback', async (req, res) => {
  try {
    const { code, state } = req.query;
    
    if (state !== req.session.oauthState) {
      return res.status(400).send('Invalid state parameter');
    }
    
    // Exchange code for token
    const tokenResponse = await axios.post(config.github.tokenUrl, {
      code: code,
      client_id: config.github.clientId,
      client_secret: config.github.clientSecret,
      redirect_uri: config.github.redirectUri
    }, {
      headers: { Accept: 'application/json' }
    });
    
    const { access_token } = tokenResponse.data;
    
    // Get user info
    const [userResponse, emailsResponse] = await Promise.all([
      axios.get(config.github.userInfoUrl, {
        headers: { Authorization: `Bearer ${access_token}` }
      }),
      axios.get('https://api.github.com/user/emails', {
        headers: { Authorization: `Bearer ${access_token}` }
      })
    ]);
    
    const user = userResponse.data;
    const primaryEmail = emailsResponse.data.find(e => e.primary)?.email || emailsResponse.data[0]?.email;
    
    req.session.user = {
      id: user.id.toString(),
      email: primaryEmail || user.email,
      name: user.name || user.login,
      picture: user.avatar_url,
      provider: 'github'
    };
    req.session.accessToken = access_token;
    
    delete req.session.oauthState;
    
    res.redirect('/dashboard');
    
  } catch (error) {
    console.error('GitHub OAuth error:', error.response?.data || error.message);
    res.status(500).send('Authentication failed');
  }
});

// Logout
app.get('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      console.error('Session destroy error:', err);
    }
    res.redirect('/');
  });
});

// Protected route example
app.get('/dashboard', (req, res) => {
  if (!req.session.user) {
    return res.redirect('/');
  }
  res.json({ user: req.session.user });
});

Using Passport.js

For simpler setup, use Passport.js with provider strategies:

// server-passport.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const GitHubStrategy = require('passport-github2').Strategy;

const app = express();

// Session configuration
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

app.use(passport.initialize());
app.use(passport.session());

// Serialize user to session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

// Deserialize user from session
passport.deserializeUser(async (id, done) => {
  // Look up user in your database
  const user = await findUserById(id);
  done(null, user);
});

// Google Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: '/auth/google/callback'
  },
  (accessToken, refreshToken, profile, done) => {
    // Find or create user in your database
    User.findOrCreate({ providerId: profile.id, provider: 'google' }, {
      email: profile.emails[0].value,
      name: profile.displayName,
      picture: profile.photos[0]?.value
    })
    .then((user) => done(null, user))
    .catch((err) => done(err));
  }
));

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

// Auth routes
app.get('/auth/google', passport.authenticate('google', {
  scope: ['profile', 'email']
}));

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => res.redirect('/dashboard')
);

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

app.get('/auth/github/callback',
  passport.authenticate('github', { failureRedirect: '/login' }),
  (req, res) => res.redirect('/dashboard')
);

Client-Side Implementation

React OAuth Implementation

// AuthProvider.jsx
import React, { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

const OAUTH_CONFIG = {
  google: {
    clientId: process.env.REACT_APP_GOOGLE_CLIENT_ID,
    redirectUri: `${window.location.origin}/oauth/callback`,
    scope: 'openid email profile',
    authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth'
  },
  github: {
    clientId: process.env.REACT_APP_GITHUB_CLIENT_ID,
    redirectUri: `${window.location.origin}/oauth/callback`,
    scope: 'read:user user:email',
    authorizationUrl: 'https://github.com/login/oauth/authorize'
  }
};

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

async function generateCodeVerifier() {
  const array = new Uint8Array(32);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

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

function generateState() {
  const array = new Uint8Array(16);
  crypto.getRandomValues(array);
  return base64UrlEncode(array);
}

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Check for existing session on mount
    checkAuth();
  }, []);

  const checkAuth = async () => {
    try {
      const response = await fetch('/api/auth/me', {
        credentials: 'include'
      });
      if (response.ok) {
        const userData = await response.json();
        setUser(userData);
      }
    } catch (error) {
      console.error('Auth check failed:', error);
    } finally {
      setLoading(false);
    }
  };

  const loginWithGoogle = async () => {
    const codeVerifier = await generateCodeVerifier();
    const codeChallenge = await generateCodeChallenge(codeVerifier);
    const state = generateState();

    // Store PKCE values for callback
    sessionStorage.setItem('oauth_code_verifier', codeVerifier);
    sessionStorage.setItem('oauth_state', state);
    sessionStorage.setItem('oauth_provider', 'google');

    const config = OAUTH_CONFIG.google;
    const params = new URLSearchParams({
      client_id: config.clientId,
      redirect_uri: config.redirectUri,
      response_type: 'code',
      scope: config.scope,
      code_challenge: codeChallenge,
      code_challenge_method: 'S256',
      state: state
    });

    window.location.href = `${config.authorizationUrl}?${params}`;
  };

  const loginWithGithub = () => {
    const state = generateState();
    sessionStorage.setItem('oauth_state', state);
    sessionStorage.setItem('oauth_provider', 'github');

    const config = OAUTH_CONFIG.github;
    const params = new URLSearchParams({
      client_id: config.clientId,
      redirect_uri: config.redirectUri,
      scope: config.scope,
      state: state
    });

    window.location.href = `${config.authorizationUrl}?${params}`;
  };

  const logout = async () => {
    await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, loading, loginWithGoogle, loginWithGithub, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

OAuth Callback Handler

// OAuthCallback.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

export function OAuthCallback() {
  const navigate = useNavigate();

  useEffect(() => {
    handleCallback();
  }, []);

  const handleCallback = async () => {
    const params = new URLSearchParams(window.location.search);
    const code = params.get('code');
    const state = params.get('state');
    const error = params.get('error');

    if (error) {
      console.error('OAuth error:', error);
      navigate('/login?error=oauth_failed');
      return;
    }

    if (!code) {
      navigate('/login?error=no_code');
      return;
    }

    // Verify state
    const storedState = sessionStorage.getItem('oauth_state');
    const provider = sessionStorage.getItem('oauth_provider');
    
    if (state !== storedState) {
      navigate('/login?error=invalid_state');
      return;
    }

    try {
      // Exchange code for session
      const response = await fetch('/api/auth/oauth/callback', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          code,
          provider,
          codeVerifier: sessionStorage.getItem('oauth_code_verifier')
        }),
        credentials: 'include'
      });

      if (!response.ok) {
        throw new Error('Authentication failed');
      }

      // Clean up storage
      sessionStorage.removeItem('oauth_state');
      sessionStorage.removeItem('oauth_code_verifier');
      sessionStorage.removeItem('oauth_provider');

      navigate('/dashboard');
    } catch (error) {
      console.error('Callback error:', error);
      navigate('/login?error=authentication_failed');
    }
  };

  return <div>Authenticating...</div>;
}

Login Component

// LoginPage.jsx
import { useAuth } from './AuthProvider';

export function LoginPage() {
  const { loginWithGoogle, loginWithGithub } = useAuth();

  return (
    <div className="login-page">
      <h1>Welcome</h1>
      <p>Sign in to continue</p>

      <div className="oauth-buttons">
        <button onClick={loginWithGoogle} className="btn-google">
          <img src="/google-icon.svg" alt="Google" />
          Continue with Google
        </button>

        <button onClick={loginWithGithub} className="btn-github">
          <img src="/github-icon.svg" alt="GitHub" />
          Continue with GitHub
        </button>
      </div>

      <div className="divider">
        <span>or</span>
      </div>

      <form className="email-login">
        <input type="email" placeholder="Email" />
        <button type="submit">Continue with Email</button>
      </form>
    </div>
  );
}

Token Management

Token Refresh

// tokenManager.js
class TokenManager {
  constructor(axiosInstance) {
    this.axios = axiosInstance;
    this.accessToken = null;
    this.refreshToken = null;
    this.tokenExpiry = null;
  }

  setTokens(accessToken, refreshToken, expiresIn) {
    this.accessToken = accessToken;
    this.refreshToken = refreshToken;
    this.tokenExpiry = Date.now() + expiresIn * 1000;
  }

  async getValidToken() {
    // Return current token if still valid
    if (this.accessToken && Date.now() < this.tokenExpiry - 60000) {
      return this.accessToken;
    }

    // Refresh token if available
    if (this.refreshToken) {
      try {
        const response = await this.axios.post('/api/auth/refresh', {
          refresh_token: this.refreshToken
        });

        this.setTokens(
          response.data.access_token,
          response.data.refresh_token,
          response.data.expires_in
        );

        return this.accessToken;
      } catch (error) {
        // Refresh failed, need to re-authenticate
        this.clearTokens();
        throw new Error('Session expired');
      }
    }

    throw new Error('No valid token');
  }

  clearTokens() {
    this.accessToken = null;
    this.refreshToken = null;
    this.tokenExpiry = null;
  }
}

// Axios interceptor for automatic token handling
function setupAxiosInterceptors(axiosInstance, tokenManager) {
  axiosInstance.interceptors.request.use(
    async (config) => {
      const token = await tokenManager.getValidToken();
      config.headers.Authorization = `Bearer ${token}`;
      return config;
    },
    (error) => Promise.reject(error)
  );

  axiosInstance.interceptors.response.use(
    (response) => response,
    async (error) => {
      const originalRequest = error.config;

      // Try to refresh on 401
      if (error.response?.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;

        try {
          const token = await tokenManager.getValidToken();
          originalRequest.headers.Authorization = `Bearer ${token`;
          return axiosInstance(originalRequest);
        } catch (refreshError) {
          // Redirect to login
          window.location.href = '/login';
          return Promise.reject(refreshError);
        }
      }

      return Promise.reject(error);
    }
  );
}

Secure Token Storage

// Frontend: Never store tokens in localStorage
// Use httpOnly cookies instead

// Server: Set tokens as httpOnly cookies
app.post('/auth/oauth/callback', async (req, res) => {
  const { access_token, refresh_token } = await exchangeCodeForTokens(req.body);

  // Set httpOnly cookies
  res.cookie('access_token', access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 60 * 60 * 1000 // 1 hour
  });

  res.cookie('refresh_token', refresh_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });

  res.json({ success: true });
});

// Refresh endpoint
app.post('/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  
  const newTokens = await refreshAccessToken(refreshToken);
  
  res.cookie('access_token', newTokens.access_token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 1000
  });

  res.json({
    access_token: newTokens.access_token,
    expires_in: 3600
  });
});

Provider-Specific Setup

Google OAuth Setup

  1. Go to Google Cloud Console
  2. Create a new project
  3. Navigate to APIs & Services > Credentials
  4. Create OAuth 2.0 Client ID
  5. Configure authorized redirect URIs
  6. Get Client ID and Client Secret
// Environment variables
GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-your-secret

GitHub OAuth Setup

  1. Go to GitHub Settings > Developer settings > OAuth Apps
  2. Click “New OAuth App”
  3. Fill in application details
  4. Get Client ID and generate Client Secret
// Environment variables
GITHUB_CLIENT_ID=Iv1.xxx
GITHUB_CLIENT_SECRET=xxx

Facebook OAuth Setup

app.get('/auth/facebook', (req, res) => {
  const facebookConfig = {
    clientId: process.env.FACEBOOK_CLIENT_ID,
    redirectUri: 'http://localhost:3000/auth/facebook/callback',
    scope: 'email,public_profile'
  };
  
  const params = new URLSearchParams({
    client_id: facebookConfig.clientId,
    redirect_uri: facebookConfig.redirectUri,
    scope: facebookConfig.scope,
    response_type: 'code',
    state: generateState()
  });
  
  res.redirect(`https://www.facebook.com/v18.0/dialog/oauth?${params}`);
});

Security Best Practices

Essential Security Measures

const securityConfig = {
  // 1. Use PKCE for all OAuth flows
  // Already implemented in code above
  
  // 2. Validate state parameter
  function validateState(req, providedState) {
    const storedState = req.session.oauthState;
    if (!storedState || storedState !== providedState) {
      throw new Error('Invalid state - possible CSRF attack');
    }
  }

  // 3. Use short-lived tokens
  tokenConfig: {
    accessTokenExpiry: 3600,    // 1 hour
    refreshTokenExpiry: 604800, // 7 days
    idTokenExpiry: 3600         // 1 hour
  },

  // 4. Implement token rotation
  async rotateRefreshToken(oldRefreshToken) {
    const response = await axios.post(tokenUrl, {
      grant_type: 'refresh_token',
      refresh_token: oldRefreshToken,
      client_id: clientId
    });
    
    // New refresh token should be used
    return response.data;
  },

  // 5. Use httpOnly cookies for tokens
  cookieOptions: {
    httpOnly: true,
    secure: true, // HTTPS only in production
    sameSite: 'strict',
    path: '/'
  }
};

// Rate limiting for OAuth endpoints
const rateLimit = require('express-rate-limit');

const oauthLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 20, // 20 attempts per window
  message: 'Too many OAuth attempts, please try again later'
});

app.use('/auth/', oauthLimiter);

Token Validation

// Server: Validate ID token with Google
const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);

async function verifyGoogleIdToken(idToken) {
  try {
    const ticket = await client.verifyIdToken({
      idToken: idToken,
      audience: process.env.GOOGLE_CLIENT_ID
    });
    
    const payload = ticket.getPayload();
    return {
      valid: true,
      user: {
        id: payload.sub,
        email: payload.email,
        name: payload.name,
        picture: payload.picture
      }
    };
  } catch (error) {
    return { valid: false, error: error.message };
  }
}

// Verify access token
async function verifyGoogleAccessToken(accessToken) {
  try {
    const response = await axios.get(
      'https://www.googleapis.com/oauth2/v3/tokeninfo',
      { params: { access_token: accessToken } }
    );
    return response.data;
  } catch (error) {
    return null;
  }
}

Handling Edge Cases

Error Handling

// OAuth error types
const OAUTH_ERRORS = {
  // Authorization errors
  access_denied: 'User denied access',
  invalid_request: 'Malformed request',
  unauthorized_client: 'Client not authorized',
  unsupported_response_type: 'Response type not supported',
  invalid_scope: 'Invalid scope requested',
  server_error: 'Authorization server error',
  temporarily_unavailable: 'Server temporarily unavailable',

  // Token errors
  invalid_grant: 'Invalid or expired authorization code',
  invalid_client: 'Invalid client credentials',
  invalid_token: 'Invalid or expired token',
  unsupported_grant_type: 'Grant type not supported'
};

app.get('/auth/:provider/callback', (req, res) => {
  const { error, error_description, code, state } = req.query;
  
  if (error) {
    const errorMessage = OAUTH_ERRORS[error] || error_description || 'Authentication failed';
    return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}`);
  }
  
  if (!code) {
    return res.redirect('/login?error=no_authorization_code');
  }
  
  // Continue with token exchange...
});

Account Linking

// Link OAuth account to existing user
async function linkOAuthAccount(userId, provider, providerId, email) {
  const existingOAuth = await OAuthAccount.findOne({ provider, providerId });
  
  if (existingOAuth) {
    if (existingOAuth.userId.toString() !== userId) {
      throw new Error('This OAuth account is already linked to another user');
    }
    // Already linked to current user
    return existingOAuth;
  }
  
  // Create new link
  return OAuthAccount.create({
    userId,
    provider,
    providerId,
    email,
    linkedAt: new Date()
  });
}

// Handle login with account that has multiple OAuth providers
async function findOrCreateUser(profile, provider) {
  // Check if email exists with different provider
  const existingEmail = await User.findOne({ 
    email: profile.email,
    _id: { $ne: profile._id }
  });
  
  if (existingEmail) {
    // Prompt user to link accounts
    return { needsLinking: true, existingUser: existingEmail, newProfile: profile };
  }
  
  // Check if OAuth account exists
  const existingOAuth = await OAuthAccount.findOne({
    provider,
    providerId: profile.providerId
  });
  
  if (existingOAuth) {
    // Return existing user
    return { user: await User.findById(existingOAuth.userId) };
  }
  
  // Create new user
  const newUser = await User.create({
    email: profile.email,
    name: profile.name,
    picture: profile.picture,
    verified: true // OAuth providers verify email
  });
  
  await OAuthAccount.create({
    userId: newUser._id,
    provider,
    providerId: profile.providerId
  });
  
  return { user: newUser };
}

Conclusion

OAuth implementation requires attention to security details:

  1. Always use PKCE - Protects against authorization code interception
  2. Validate state parameter - Prevents CSRF attacks
  3. Use httpOnly cookies - Never store tokens in localStorage
  4. Implement token refresh - Maintains seamless user experience
  5. Handle errors gracefully - Provide clear feedback to users
  6. Link accounts carefully - Handle multiple OAuth providers for same email

For production deployments, consider using established libraries like:

  • Auth0 - Full-featured identity platform
  • Clerk - Modern authentication for React apps
  • NextAuth.js - Authentication for Next.js applications
  • Supabase Auth - Open-source Firebase alternative

Resources

Comments