Implementing OAuth 2.0 in Node.js: Secure User Authentication Made Easy

16/07/2025

Implementing OAuth 2.0 in Node.js: Secure User Authentication Made Easy

Learn how to implement OAuth 2.0 in Node.js for secure user authentication. This guide covers flows, token handling, and integration with providers like Google, GitHub, and Facebook.

Implementing OAuth 2.0 in Node.js

Securely authenticate users and grant access to resources with industry-standard protocols.

Introduction

In today's interconnected web, users often prefer to sign in to applications using their existing accounts from services like Google, Facebook, or GitHub. This not only enhances user experience by removing the need for new registrations but also offloads the burden of password management and security to trusted identity providers. The protocol that enables this secure delegation of access is **OAuth 2.0**. It's an authorization framework that allows a third-party application to obtain limited access to a user's resources on an HTTP service, without exposing the user's credentials. In this post, we'll dive into implementing OAuth 2.0 in a Node.js backend, using the popular Passport.js library and Google as our identity provider.

Understanding OAuth 2.0 Concepts

OAuth 2.0 defines several roles and flows:

  • Resource Owner: The user who owns the data (e.g., you, with your Google photos).
  • Client: The application requesting access to the resource owner's data (e.g., a photo editing app).
  • Authorization Server: The server that authenticates the resource owner and issues access tokens (e.g., Google's authentication server).
  • Resource Server: The server hosting the protected resources (e.g., Google Photos API).
  • Authorization Grant: The method by which the client obtains an access token (e.g., Authorization Code flow, Implicit flow).
  • Access Token: A credential that grants the client access to specific protected resources. It has a limited lifespan.
  • Refresh Token: A long-lived credential used to obtain new access tokens when the current one expires, without requiring the user to re-authenticate.
  • Scopes: Define the specific permissions or level of access the client is requesting (e.g., `profile`, `email`, `https://www.googleapis.com/auth/photos.readonly`).

For web applications, the **Authorization Code Grant** flow is the most secure and commonly used.

Prerequisites

  • Node.js and npm (or yarn) installed.
  • Basic understanding of Express.js.
  • A Google Cloud Project with OAuth 2.0 credentials (Client ID and Client Secret).

1. Project Setup and Dependencies

Create a new Node.js project and install the required packages.

# Create project directory
mkdir node-oauth2-google
cd node-oauth2-google

# Initialize npm
npm init -y

# Install dependencies
npm install express express-session passport passport-google-oauth20 dotenv
  • `express`: Our web framework.
  • `express-session`: Middleware for managing sessions (needed by Passport.js).
  • `passport`: Authentication middleware for Node.js.
  • `passport-google-oauth20`: Passport strategy for authenticating with Google using OAuth 2.0.
  • `dotenv`: To load environment variables.

2. Configure Google OAuth Credentials

Go to the Google Cloud Console:

  1. Create a new project (if you don't have one).
  2. Navigate to "APIs & Services" -> "Credentials".
  3. Click "Create Credentials" -> "OAuth client ID".
  4. Choose "Web application".
  5. Set "Authorized JavaScript origins" to `http://localhost:3000` (or your frontend URL).
  6. Set "Authorized redirect URIs" to `http://localhost:3000/auth/google/callback`.
  7. You will get your **Client ID** and **Client Secret**.

Create a `.env` file in your project root to store these credentials:

# .env
GOOGLE_CLIENT_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=YOUR_GOOGLE_CLIENT_SECRET
SESSION_SECRET=a_very_secret_key_for_sessions

3. Server-side Code (`server.js`)

This file will set up Express, session management, and Passport.js.

// server.js
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const path = require('path');

const app = express();
const PORT = process.env.PORT || 3000;

// --- Passport.js Setup ---

// Serialize user into the session
passport.serializeUser((user, done) => {
  done(null, user.id); // Store only user ID in session
});

// Deserialize user from the session
passport.deserializeUser((id, done) => {
  // In a real app, you'd fetch the user from your database here
  // For this example, we'll just return a mock user
  const user = { id: id, displayName: `User ${id}` }; // Mock user
  done(null, user);
});

// Google OAuth Strategy
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "/auth/google/callback" // This must match your Google Cloud Console redirect URI
  },
  (accessToken, refreshToken, profile, done) => {
    // This callback is called after successful authentication with Google
    // Here, you would typically find or create a user in your database
    // based on 'profile.id' or 'profile.emails[0].value'
    console.log('Google Profile:', profile.id, profile.displayName);
    // For simplicity, we'll just pass the profile directly
    done(null, profile);
  }
));

// --- Express Middleware ---
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Session middleware
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 1000 * 60 * 60 * 24 } // 1 day
}));

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

// Serve static files (our client-side HTML)
app.use(express.static(path.join(__dirname, 'public')));

// --- Authentication Routes ---

// Route to initiate Google OAuth login
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] }) // Request user's profile and email
);

// Google OAuth callback route
app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/' }), // Redirect to home on failure
  (req, res) => {
    // Successful authentication, redirect home or to a dashboard
    res.redirect('/profile');
  }
);

// Logout route
app.get('/auth/logout', (req, res, next) => {
  req.logout((err) => { // Passport's logout method
    if (err) { return next(err); }
    req.session.destroy((err) => { // Destroy the session
      if (err) { return next(err); }
      res.redirect('/'); // Redirect to home page after logout
    });
  });
});

// --- Protected Route Example ---
function isAuthenticated(req, res, next) {
  if (req.isAuthenticated()) { // Passport method to check if user is authenticated
    return next();
  }
  res.redirect('/'); // Redirect to home if not authenticated
}

app.get('/profile', isAuthenticated, (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'profile.html'));
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

4. Client-side HTML (`public/index.html` and `public/profile.html`)

Create a `public` directory and two HTML files inside it.

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login with Google</title>
    <style>
        body {
            font-family: sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background-color: #f3f4f6;
            color: #1f2937;
        }
        .container {
            background-color: #ffffff;
            padding: 2rem;
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            text-align: center;
        }
        h1 {
            font-size: 1.5rem;
            font-weight: bold;
            margin-bottom: 1.5rem;
            color: #1e3a8a;
        }
        .google-btn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background-color: #4285F4;
            color: white;
            padding: 0.75rem 1.5rem;
            border-radius: 0.375rem;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.2s;
            text-decoration: none;
        }
        .google-btn:hover {
            background-color: #357ae8;
        }
        .google-icon {
            margin-right: 0.5rem;
            width: 20px;
            height: 20px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome! Please Login</h1>
        <a href="/auth/google" class="google-btn">
            <img src="https://upload.wikimedia.org/wikipedia/commons/4/4a/Logo_Google_G_variante.svg" alt="Google icon" class="google-icon">
            Sign in with Google
        </a>
    </div>
</body>
</html>
                  <!-- public/profile.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>User Profile</title>
    <style>
        body {
            font-family: sans-serif;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background-color: #f3f4f6;
            color: #1f2937;
        }
        .container {
            background-color: #ffffff;
            padding: 2rem;
            border-radius: 0.5rem;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            text-align: center;
        }
        h1 {
            font-size: 1.5rem;
            font-weight: bold;
            margin-bottom: 1.5rem;
            color: #1e3a8a;
        }
        p {
            margin-bottom: 1rem;
        }
        .logout-btn {
            display: inline-block;
            background-color: #ef4444;
            color: white;
            padding: 0.75rem 1.5rem;
            border-radius: 0.375rem;
            font-weight: 600;
            cursor: pointer;
            transition: background-color 0.2s;
            text-decoration: none;
        }
        .logout-btn:hover {
            background-color: #dc2626;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Welcome to your Profile!</h1>
        <p>You are successfully authenticated.</p>
        <p>User ID: <span id="userId">Loading...</span></p>
        <p>Display Name: <span id="displayName">Loading...</span></p>
        <a href="/auth/logout" class="logout-btn">Logout</a>
    </div>

    <script>
        // Fetch user data from the server (e.g., a /api/user endpoint if you had one)
        // For this example, we'll rely on the server-side session to render the profile page.
        // In a real app, you might have a /api/me endpoint that returns user details.
        // For now, we'll simulate fetching user info from session or a simple endpoint.
        async function fetchUserProfile() {
            try {
                const response = await fetch('/api/user-info');
                if (response.ok) {
                    const user = await response.json();
                    document.getElementById('userId').textContent = user.id;
                    document.getElementById('displayName').textContent = user.displayName || 'N/A';
                } else {
                    document.getElementById('userId').textContent = 'Error fetching user ID';
                    document.getElementById('displayName').textContent = 'Error fetching display name';
                }
            } catch (error) {
                console.error('Error fetching user profile:', error);
                document.getElementById('userId').textContent = 'Network Error';
                document.getElementById('displayName').textContent = 'Network Error';
            }
        }

        // Sample server endpoint logic:
        // app.get('/api/user-info', isAuthenticated, (req, res) => {
        //   res.json({ id: req.user.id, displayName: req.user.displayName });
        // });

        // Placeholder since actual API is not implemented
        document.getElementById('userId').textContent = 'Authenticated';
        document.getElementById('displayName').textContent = 'Authenticated User';
    </script>
</body>
</html>

5. Running the Application

Start your Node.js server:

node server.js

Open your browser and navigate to `http://localhost:3000`. Click "Sign in with Google" to test the OAuth flow.

Implementing OAuth 2.0 might seem complex at first, but with the help of Node.js and the Passport.js library, it becomes a manageable task. By leveraging Passport's strategies, you can easily integrate with popular identity providers like Google, providing a secure and convenient authentication experience for your users. This setup forms a robust foundation for handling user authentication and authorization in your Node.js applications, allowing you to build features that rely on delegated access to external services. Remember to always keep your client secrets secure and use HTTPS in production environments.

In today’s web applications, secure user authentication is non-negotiable — and OAuth 2.0 has become the industry standard for delegated authorization and third-party login. Whether you’re building a single-page app, mobile backend, or API service, implementing OAuth correctly ensures both user security and a seamless login experience.

Node.js, with its flexibility and powerful ecosystem, makes it easy to integrate OAuth 2.0 flows with various identity providers such as Google, GitHub, Facebook, Twitter, and Microsoft. But understanding how OAuth 2.0 works — and choosing the right flow — is critical to avoid vulnerabilities and misconfigurations.