Streamlining Social Authentication in Node.js with Passport.js

Wajahat Hussain
Coinmonks
Published in
9 min read3 days ago

--

Introduction

Passport.js is the most popular authentication library for Node.js, well-known in the community and successfully used in many production applications. Many Node.js applications require users to authenticate to access private content. The authentication process must be both functional and secure. This article will guide you through implementing social authentication in a Node.js and Express application using Passport.js, covering six major providers: Facebook, Google, Twitter, LinkedIn, Instagram, and Discord.

What is Passport.js?

Passport.js is a simple, unobtrusive authentication middleware for Node.js that can be seamlessly integrated into any Express.js-based web application. Its modular design allows you to add or switch authentication methods without altering your application’s core logic.

Key Components and Concepts of Passport Authentication

  1. Authentication Strategies: Passport supports multiple authentication strategies, each corresponding to a different method of authentication, such as username/password, OAuth, JWT (JSON Web Tokens), and more. Each strategy is encapsulated in a separate module, making it easy to manage various authentication methods.
  2. Middleware Integration: Passport is integrated as middleware in your Express application’s request/response cycle. It is added to the pipeline before the routes that require authentication, authenticating the user’s request and attaching the user object to the request.
  3. Serialization and Deserialization: Passport provides methods for serializing and deserializing user objects. Serialization transforms the user object into a format that can be stored in a session or a token, while deserialization retrieves the user object from the session or token.
  4. Authentication Flow: Passport simplifies the authentication process into a series of straightforward steps. Typically, a user’s authentication request is handled by a route or controller, and Passport processes the authentication strategy. If successful, it attaches the authenticated user to the request; if authentication fails, Passport handles the failure gracefully.
  5. Session Management: Passport can work with session-based authentication, storing user data in a session on the server after successful authentication. This allows the user to remain authenticated across subsequent requests until the session expires or is explicitly terminated.
  6. Error Handling: Passport provides mechanisms for handling authentication failures and errors, allowing you to customize your application’s responses to authentication failures or unauthorized access attempts.

Setting Up Passport in Your Node.js Application

To use Passport in your Node.js application, follow these steps:

Step 1: Create a New Project and Install Dependencies

Open your terminal and create a new project folder. Navigate to the project folder and initialize a new Node.js project:

mkdir passport-backend
cd passport-backend
npm init -y

Install the required dependencies for Passport and the specific authentication strategies:

npm i express passport cors axios passport-google-oauth20 passport-facebook @superfaceai/passport-twitter-oauth2 passport-discord github:auth0/passport-linkedin-oauth2#v3.0.0 passport-instagram express morgan dotenv crypto-js express-session

Step 2: Create an Express Application and Set Up Passport and Express Sessions

Create a server.js file in your project folder and set up the basic Express application structure. Configure Passport and Express sessions by adding these lines before your app starts listening:

import express from "express";
import cors from "cors";
import session from "express-session";
import passport from "./controller/services/passport/index.js"; // Assuming your Passport configuration is exported from this path
import requestLogger from "./controller/middlewares/requestLogger.js";
import errorLogger from "./controller/middlewares/errorLogger.js";
import errorHandler from "./controller/middlewares/errorHandler.js";
import { authEndpoints } from "./routes/authEndpoints.js"; // Assuming you have defined your authentication endpoints in this file
import dotenv from "dotenv";
dotenv.config();

const app = express();

// Middleware setup
app.use(cors());
app.use(express.json({ limit: "5mb" })); // JSON body parser with a limit
app.use(
express.urlencoded({
limit: "5mb",
extended: true,
})
); // URL-encoded body parser with a limit
app.use(
session({
secret: "keyboard cat", // Session secret (replace with a more secure secret)
resave: false,
saveUninitialized: true,
cookie: { secure: false }, // Set to true if using HTTPS
})
);
app.use(requestLogger); // Custom middleware for logging requests

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

// Register Authentication Routes
authEndpoints.forEach(({ path, method, handler, postAuth }) => {
app[method]( // Register each endpoint with its method and handler
path,
handler,
postAuth ? postAuth : (req, res) => res.json({ user: req.user }) // Default handler to respond with user info after authentication
);
});

// Error handling middleware
app.use(errorLogger); // Custom middleware for logging errors
app.use(errorHandler); // Global error handler middleware

const PORT = process.env.PORT || 9900; // Default port or from environment variable
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

Step 3: Setting Up Passport.js Strategies

Implement the necessary Passport.js strategies for social authentication (Facebook, Google, Twitter, LinkedIn, Instagram, Discord). Below is a basic structure with Serialize and Deserialize Users
passport/strategies/google.js file:

import passport from "passport";
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import dotenv from "dotenv";
dotenv.config();

// Configure Google OAuth strategy
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID, // Google Client ID
clientSecret: process.env.GOOGLE_CLIENT_SECRET, // Google Client Secret
callbackURL: "/auth/google/callback", // Callback URL after authentication
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
let result = profile?._json; // Extract JSON data from profile

// Prepare data object with extracted profile information
let data = {
googleId: profile.id, // Google user ID
userName: result.name, // User's name
...(result?.email && { email: result?.email }), // Include email if available
...(result?.picture && { profilePic: result?.picture }), // Include profile picture if available
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/strategies/facebook.js file:

import passport from "passport";
import { Strategy as FacebookStrategy } from "passport-facebook";

// Configure Facebook OAuth strategy
passport.use(
new FacebookStrategy(
{
clientID: process.env.FACEBOOK_CLIENT_ID, // Facebook App ID
clientSecret: process.env.FACEBOOK_CLIENT_SECRET, // Facebook App Secret
callbackURL: "/auth/facebook/callback", // Callback URL after authentication
profileFields: ["id", "displayName", "picture", "email"], // Fields to retrieve from user profile
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
let result = profile?._json; // Extract JSON data from profile

// Prepare data object with extracted profile information
let data = {
facebookId: profile.id,
userName: result.name,
...(result?.email && { email: result?.email }), // Include email if available
...(result?.picture?.data && {
profilePic: result?.picture?.data?.url, // Include profile picture URL if available
}),
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/strategies/twitter.js file:

import passport from "passport";
import { Strategy as TwitterStrategy } from "@superfaceai/passport-twitter-oauth2";

// Configure Twitter OAuth strategy
passport.use(
new TwitterStrategy(
{
clientType: "confidential", // Client type (confidential for Twitter)
clientID: process.env.TWITTER_CLIENT_ID, // Twitter Client ID
clientSecret: process.env.TWITTER_CLIENT_SECRET, // Twitter Client Secret
callbackURL: "/auth/twitter/callback", // Callback URL after authentication
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
// Prepare data object with profile information
let data = {
twitterId: profile.id, // Twitter user ID
userName: profile.displayName, // User's display name
...(profile?.emails && { email: profile?.emails[0]?.value }), // Include email if available
...(profile?.photos && { profilePic: profile?.photos[0]?.value }), // Include profile picture if available
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/strategies/linkedin.js file:

import passport from "passport";
import { Strategy as LinkedInStrategy } from "passport-linkedin-oauth2";

// Configure LinkedIn OAuth strategy
passport.use(
new LinkedInStrategy(
{
clientID: process.env.LINKEDIN_CLIENT_ID, // LinkedIn Client ID
clientSecret: process.env.LINKEDIN_CLIENT_SECRET, // LinkedIn Client Secret
callbackURL: "/auth/linkedin/callback", // Callback URL after authentication
scope: ["openid", "profile", "email"], // LinkedIn API scopes to request
state: true, // Enables state parameter for CSRF protection
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
// Prepare data object with profile information
let data = {
linkedinId: profile.id, // LinkedIn user ID
userName: profile.displayName, // User's display name
...(profile?.email && { email: profile?.email }), // Include email if available
...(profile?.picture && { profilePic: profile?.picture }), // Include profile picture if available
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/strategies/instagram.js file:

import passport from "passport";
import { Strategy as InstagramStrategy } from "passport-instagram";

// Configure Instagram OAuth strategy
passport.use(
new InstagramStrategy(
{
clientID: process.env.INSTAGRAM_CLIENT_ID, // Instagram Client ID
clientSecret: process.env.INSTAGRAM_CLIENT_SECRET, // Instagram Client Secret
callbackURL: "/auth/instagram/callback", // Callback URL after authentication
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
// Prepare data object with profile information
let data = {
instagramId: profile.id, // Instagram user ID
userName: profile.displayName, // User's display name
...(profile?.emails && { email: profile?.emails[0]?.value }), // Include email if available
...(profile?.photos && { profilePic: profile?.photos[0]?.value }), // Include profile picture if available
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/strategies/discord.js file:

import passport from "passport";
import { Strategy as DiscordStrategy } from "passport-discord";

// Configure Discord OAuth strategy
passport.use(
new DiscordStrategy(
{
clientID: process.env.DISCORD_CLIENT_ID, // Discord App ID
clientSecret: process.env.DISCORD_CLIENT_SECRET, // Discord App Secret
callbackURL: "/auth/discord/callback", // Callback URL after authentication
scope: ["identify", "email"], // Discord API scopes to request
passReqToCallback: true, // Allows passing the request to the callback function
},
async (req, accessToken, refreshToken, profile, done) => {
try {
// Prepare data object with profile information
let data = {
discordId: profile.id, // Discord user ID
userName: profile.global_name, // Discord username
...(profile?.email && { email: profile?.email }), // Include email if available
...(profile?.avatar && {
profilePic: `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`, // Construct avatar URL
}),
accessToken, // Store access token
};

// Custom logic can be added here to process profile data as needed

// Return data to passport for authentication process completion
return done(null, data);
} catch (err) {
// Handle any errors that occur during profile processing
return done(err);
}
}
)
);

passport/index.js file:

import passport from "passport";

// Import OAuth strategies
import "./strategies/google.js";
import "./strategies/facebook.js";
import "./strategies/instagram.js";
import "./strategies/twitter.js";
import "./strategies/linkedin.js";
import "./strategies/discord.js";

// Serialize user instance to the session
passport.serializeUser((user, done) => {
// If using a database, serialize user ID instead of the entire user object
done(null, user);
});

// Deserialize user instance from the session
passport.deserializeUser((obj, done) => {
try {
// If using a database, retrieve the user from the database using the serialized user ID
done(null, obj);
} catch (error) {
done(error, null);
}
});

export default passport;

Step 4: Implement Authentication Routes

Create a routes/authEndpoints.js file in your project folder. Define the authentication routes for each provider, including the callback routes:

import passport from '../controller/services/passport/index.js'; // Assuming Passport configuration is exported from this path
import { handlePostAuthRedirect } from '../controller/services/handlePostAuthRedirect.js'; // Assuming a function to handle post-authentication redirection

// Helper function for creating authentication endpoints
const createAuthEndpoint = (provider, scope = []) => ({
path: `/auth/${provider}`, // Path for authentication endpoint
method: 'get', // HTTP method (GET for authentication flow)
handler: (req, res, next) => {
passport.authenticate(provider, { scope, passReqToCallback: true })(req, res, next); // Initiate authentication flow using Passport
},
});

// Helper function for creating callback endpoints
const createCallbackEndpoint = provider => ({
path: `/auth/${provider}/callback`, // Callback URL for the provider
method: 'get', // HTTP method (GET for callback handling)
handler: passport.authenticate(provider, { failureRedirect: process.env.FAILURE_REDIRECT_URL }), // Passport middleware to handle authentication callback
postAuth: handlePostAuthRedirect, // Function to handle post-authentication redirection
});

// Register Passport.js authentication routes
const authEndpoints = [
// Facebook authentication endpoints
createAuthEndpoint('facebook', ['email']),
createCallbackEndpoint('facebook'),

// Google authentication endpoints
createAuthEndpoint('google', ['profile', 'email']),
createCallbackEndpoint('google'),

// LinkedIn authentication endpoints
createAuthEndpoint('linkedin', ['openid', 'profile', 'email']),
createCallbackEndpoint('linkedin'),

// Discord authentication endpoints
createAuthEndpoint('discord', ['identify', 'email']),
createCallbackEndpoint('discord'),

// Twitter authentication endpoints
createAuthEndpoint('twitter', ['tweet.read', 'users.read', 'offline.access']),
createCallbackEndpoint('twitter'),

// Instagram authentication endpoints
createAuthEndpoint('instagram'),
createCallbackEndpoint('instagram'),

// Telegram authentication endpoints
createAuthEndpoint('telegram'),
createCallbackEndpoint('telegram'),
];

export { authEndpoints };

Conclusion

Passport.js simplifies the implementation of social authentication in Node.js and Express applications, providing a flexible and modular approach to managing different authentication methods. By following the steps outlined in this article, you can easily set up and manage authentication for multiple social providers, ensuring a seamless and secure user experience.

For a complete example and detailed implementation, you can refer to my GitHub repository: social-login-passport

--

--

Wajahat Hussain
Coinmonks

Blockchain enthusiast crafting innovative Dapps. Full-stack expert, backend integration, database management. Redefining with scalable solutions.