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:
- Create a new project (if you don't have one).
- Navigate to "APIs & Services" -> "Credentials".
- Click "Create Credentials" -> "OAuth client ID".
- Choose "Web application".
- Set "Authorized JavaScript origins" to `http://localhost:3000` (or your frontend URL).
- Set "Authorized redirect URIs" to `http://localhost:3000/auth/google/callback`.
- 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.