Passport.js Strategies

Flexible authentication middleware for Node.js applications

Introduction to Passport.js

Passport.js is a powerful authentication middleware for Node.js applications that simplifies the implementation of various authentication strategies. It's designed to be flexible, modular, and work with any Express-based web application.

Why Use Passport.js?

Authentication is a critical component of most web applications, and Passport makes it easier by:

  • Simplifying authentication logic with a consistent, reusable pattern
  • Supporting numerous authentication mechanisms through "strategies"
  • Separating authentication concerns from your application logic
  • Providing a unified API across different authentication methods
  • Integrating seamlessly with Express.js middleware pipeline

Think of Passport as a Swiss Army knife for authentication—one tool with many specialized attachments for different scenarios.

Core Concepts

graph TD A[Client Request] --> B[Express App] B --> C{Passport Middleware} C -->|Authenticate| D[Strategy] D -->|Success| E[Serialize User] D -->|Failure| F[Redirect to Login] E --> G[Create Session] G --> H[Protected Route] I[Subsequent Request] --> J[Express App] J --> K{Passport Middleware} K --> L[Deserialize User] L --> M[Populate req.user] M --> N[Protected Route]

Setting Up Passport.js

Let's start by setting up Passport in an Express.js application:

// Install required packages
// npm install express express-session passport

// Basic Passport setup in Express
const express = require('express');
const session = require('express-session');
const passport = require('passport');

const app = express();

// Configure session middleware (required for persistent login sessions)
app.use(session({
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Initialize Passport and restore authentication state from session
app.use(passport.initialize());
app.use(passport.session());

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

// Deserialize user from session ID
passport.deserializeUser(async (id, done) => {
  try {
    // Fetch user from database
    const user = await User.findById(id);
    done(null, user);
  } catch (err) {
    done(err);
  }
});

// Routes
app.get('/', (req, res) => {
  res.send('Home Page');
});

// Protected route example
app.get('/profile', isAuthenticated, (req, res) => {
  res.send(`Welcome, ${req.user.username}!`);
});

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

// Start server
app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Understanding the Setup

Let's break down the key components:

  • express-session: Required for persistent login sessions
  • passport.initialize(): Sets up Passport for authentication
  • passport.session(): Enables persistent login sessions
  • serializeUser(): Determines what user data is stored in the session
  • deserializeUser(): Retrieves user from database based on session data
  • req.isAuthenticated(): Checks if user is logged in

This setup establishes the foundation for any Passport-based authentication, regardless of which strategies you'll use.

Local Authentication Strategy

The Local strategy is the most basic authentication mechanism, using username and password stored in your application's database.

// Install required packages
// npm install passport-local bcrypt

const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('./models/User'); // Your user model

// Configure Local Strategy
passport.use(new LocalStrategy(
  async (username, password, done) => {
    try {
      // Find user by username
      const user = await User.findOne({ username });
      
      // User not found
      if (!user) {
        return done(null, false, { message: 'Incorrect username' });
      }
      
      // Compare password with stored hash
      const isMatch = await bcrypt.compare(password, user.password);
      
      // Password doesn't match
      if (!isMatch) {
        return done(null, false, { message: 'Incorrect password' });
      }
      
      // Authentication successful
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// Login route
app.post('/login', 
  passport.authenticate('local', { 
    successRedirect: '/profile',
    failureRedirect: '/login',
    failureFlash: true // Requires express-flash for displaying messages
  })
);

// Registration route
app.post('/register', async (req, res) => {
  try {
    const { username, email, password } = req.body;
    
    // Check if user already exists
    const existingUser = await User.findOne({ 
      $or: [{ username }, { email }] 
    });
    
    if (existingUser) {
      return res.status(400).send('Username or email already exists');
    }
    
    // Hash the password
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    
    // Create a new user
    const newUser = new User({
      username,
      email,
      password: hashedPassword
    });
    
    await newUser.save();
    
    // Redirect to login page
    res.redirect('/login');
  } catch (err) {
    res.status(500).send('Server error');
  }
});

// Logout route
app.get('/logout', (req, res) => {
  req.logout(function(err) {
    if (err) { return next(err); }
    res.redirect('/');
  });
});

How Local Authentication Works

The local authentication flow:

  1. User submits credentials via a login form
  2. Passport's middleware processes the request
  3. The LocalStrategy validates credentials against the database
  4. If valid, the user object is passed to serializeUser()
  5. The session is established with the serialized user data
  6. User is redirected to the success page

On subsequent requests, Passport retrieves the user ID from the session, uses deserializeUser() to fetch the complete user object, and makes it available as req.user.

Example User Model

// models/User.js
const mongoose = require('mongoose');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const User = mongoose.model('User', userSchema);

module.exports = User;

Custom Authentication Fields

If your login form doesn't use the default "username" and "password" field names, you can customize the LocalStrategy:

// Custom field names
passport.use(new LocalStrategy({
    usernameField: 'email',    // Using email instead of username
    passwordField: 'pass'      // Using 'pass' instead of 'password'
  },
  async (email, password, done) => {
    try {
      // Find user by email
      const user = await User.findOne({ email });
      
      // User validation logic...
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

OAuth Strategies

Passport makes implementing OAuth authentication with various providers straightforward. Let's look at how to implement Google, Facebook, and GitHub OAuth strategies:

Google OAuth 2.0

// Install required packages
// npm install passport-google-oauth20

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

// Configure Google 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']
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Check if user exists in database
      let user = await User.findOne({ googleId: profile.id });
      
      if (!user) {
        // Create new user if not found
        user = await User.create({
          googleId: profile.id,
          username: profile.displayName,
          email: profile.emails[0].value,
          picture: profile.photos[0].value
          // No password for OAuth users
        });
      }
      
      // Return user object
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

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

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

Facebook OAuth

// Install required packages
// npm install passport-facebook

const FacebookStrategy = require('passport-facebook').Strategy;

// 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', 'displayName', 'email', 'photos']
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Check if user exists
      let user = await User.findOne({ facebookId: profile.id });
      
      if (!user) {
        // Create new user
        user = await User.create({
          facebookId: profile.id,
          username: profile.displayName,
          email: profile.emails ? profile.emails[0].value : `${profile.id}@facebook.com`,
          picture: profile.photos ? profile.photos[0].value : null
        });
      }
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// Facebook Auth Routes
app.get('/auth/facebook',
  passport.authenticate('facebook', { scope: ['email'] })
);

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

GitHub OAuth

// Install required packages
// npm install passport-github2

const GitHubStrategy = require('passport-github2').Strategy;

// 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',
    scope: ['user:email']
  },
  async (accessToken, refreshToken, profile, done) => {
    try {
      // Check if user exists
      let user = await User.findOne({ githubId: profile.id });
      
      if (!user) {
        // Create new user
        user = await User.create({
          githubId: profile.id,
          username: profile.username,
          email: profile.emails ? profile.emails[0].value : `${profile.username}@github.com`,
          picture: profile.photos ? profile.photos[0].value : null
        });
      }
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

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

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

Modified User Model for OAuth

To support multiple authentication methods, your User model needs to be modified:

const userSchema = new mongoose.Schema({
  username: String,
  email: {
    type: String,
    unique: true
  },
  password: String, // May be null for OAuth users
  picture: String,
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  // OAuth provider IDs
  googleId: String,
  facebookId: String,
  githubId: String,
  
  createdAt: {
    type: Date,
    default: Date.now
  }
});

This allows a single user to authenticate through multiple methods, with the email address typically used as the common identifier.

Account Linking

Users often want to connect multiple authentication methods to a single account. Here's how to implement account linking with Passport:

// Account linking - connecting OAuth provider to existing account
app.get('/connect/google',
  // Ensure user is already authenticated
  isAuthenticated,
  // Start Google OAuth flow, but set it to connect mode
  passport.authorize('google', { scope: ['profile', 'email'] })
);

app.get('/connect/google/callback',
  isAuthenticated,
  passport.authorize('google', {
    successRedirect: '/profile',
    failureRedirect: '/profile'
  })
);

// Modified Google Strategy for account linking
passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: 'http://localhost:3000/connect/google/callback',
    passReqToCallback: true // Pass request to callback
  },
  async (req, accessToken, refreshToken, profile, done) => {
    try {
      // Check if there's an authenticated user (account linking case)
      if (req.user) {
        // Add Google ID to existing user
        const existingUser = req.user;
        existingUser.googleId = profile.id;
        
        // Optionally, update user profile with additional info
        if (!existingUser.picture && profile.photos && profile.photos.length) {
          existingUser.picture = profile.photos[0].value;
        }
        
        await existingUser.save();
        return done(null, existingUser);
      }
      
      // Normal authentication case (not account linking)
      // ... existing authentication logic
    } catch (err) {
      return done(err);
    }
  }
));

// Route to unlink accounts
app.get('/unlink/google', isAuthenticated, async (req, res) => {
  try {
    const user = await User.findById(req.user.id);
    user.googleId = undefined;
    await user.save();
    res.redirect('/profile');
  } catch (err) {
    res.status(500).send('Error unlinking account');
  }
});

Account Linking vs. Authentication

Note the key differences between linking and authentication:

  • Authentication (passport.authenticate()) creates a new user session if none exists
  • Authorization (passport.authorize()) connects a provider to an already authenticated user
  • We use passReqToCallback: true to access the existing user in the strategy
  • Different callback URLs are typically used to distinguish these flows

This approach allows users to sign in with different methods and maintain a single account.

JWT Strategy

For stateless API authentication, Passport provides a JWT strategy that works well with token-based authentication:

// Install required packages
// npm install passport-jwt jsonwebtoken

const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

// JWT Strategy options
const jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), // Extract token from Authorization header
  secretOrKey: process.env.JWT_SECRET // Secret key for verification
};

// Configure JWT Strategy
passport.use(new JwtStrategy(jwtOptions, async (payload, done) => {
  try {
    // Find user by ID in payload
    const user = await User.findById(payload.sub);
    
    if (!user) {
      return done(null, false);
    }
    
    return done(null, user);
  } catch (err) {
    return done(err, false);
  }
}));

// Route to generate JWT token (typically after login)
app.post('/api/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Find user
    const user = await User.findOne({ email });
    
    // Check if user exists and password matches
    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }
    
    // Generate JWT token
    const token = jwt.sign(
      { sub: user._id },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    // Return token to client
    res.json({ token });
  } catch (err) {
    res.status(500).json({ message: 'Server error' });
  }
});

// Protected API route using JWT authentication
app.get('/api/profile',
  passport.authenticate('jwt', { session: false }), // Don't create session
  (req, res) => {
    res.json(req.user);
  }
);

JWT Strategy Features

The JWT strategy differs from other strategies:

  • It's stateless - no session is created, suitable for APIs
  • Authentication happens on every request by verifying the token
  • Allows different extraction methods for the token (header, query parameter, cookie)
  • Enables microservice architecture where different services can validate the same token

Note the { session: false } option which tells Passport not to create a session, maintaining stateless authentication.

JWT Extraction Methods

// Different JWT extraction methods
const jwtOptions = {
  // From Authorization header (Bearer scheme)
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  
  // From Authorization header (custom scheme)
  // jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('JWT'),
  
  // From a specific query parameter
  // jwtFromRequest: ExtractJwt.fromUrlQueryParameter('token'),
  
  // From a cookie
  // jwtFromRequest: ExtractJwt.fromExtractors([
  //   (req) => {
  //     let token = null;
  //     if (req && req.cookies) {
  //       token = req.cookies['jwt'];
  //     }
  //     return token;
  //   }
  // ]),
  
  secretOrKey: process.env.JWT_SECRET
};

Custom Strategies

Passport allows you to create custom authentication strategies for specific requirements. Let's implement a simple API key strategy:

// Creating a custom API Key strategy
const passport = require('passport');
const { Strategy } = require('passport-strategy');

// Define custom ApiKeyStrategy
class ApiKeyStrategy extends Strategy {
  constructor(options, verify) {
    super();
    this.name = 'apikey'; // Strategy name
    this.verify = verify;
    this.apiKeyField = options.apiKeyField || 'api_key';
    this.passReqToCallback = options.passReqToCallback;
  }
  
  authenticate(req) {
    // Get API key from request
    const apiKey = req.get('X-API-Key') || req.query[this.apiKeyField] || req.body[this.apiKeyField];
    
    // No API key provided
    if (!apiKey) {
      return this.fail({ message: 'No API key provided' }, 401);
    }
    
    // Create verify callback
    const verified = (err, user, info) => {
      if (err) { return this.error(err); }
      if (!user) { return this.fail(info); }
      this.success(user, info);
    };
    
    // Execute verify function
    if (this.passReqToCallback) {
      this.verify(req, apiKey, verified);
    } else {
      this.verify(apiKey, verified);
    }
  }
}

// Register the strategy
passport.use(new ApiKeyStrategy(
  { apiKeyField: 'api_key' },
  async (apiKey, done) => {
    try {
      // Find API client by key
      const apiClient = await ApiClient.findOne({ apiKey });
      
      if (!apiClient) {
        return done(null, false, { message: 'Unknown API key' });
      }
      
      if (!apiClient.active) {
        return done(null, false, { message: 'API key is inactive' });
      }
      
      // Log API key usage
      apiClient.lastUsed = new Date();
      await apiClient.save();
      
      return done(null, apiClient);
    } catch (err) {
      return done(err);
    }
  }
));

// API route using custom strategy
app.get('/api/data',
  passport.authenticate('apikey', { session: false }),
  (req, res) => {
    res.json({ data: 'Protected API data' });
  }
);

When to Create Custom Strategies

Consider creating a custom strategy when:

  • You need a specialized authentication method not covered by existing strategies
  • Your application has unique security requirements
  • You're integrating with a proprietary authentication system
  • You want to validate tokens or credentials in a custom way

Custom strategies follow the same pattern as built-in strategies, making them consistent and reusable across applications.

Multiple Strategy Configuration

Most applications benefit from supporting multiple authentication strategies. Here's how to implement and manage them effectively:

// Configuring multiple strategies

// Local Strategy
passport.use('local', new LocalStrategy(
  // Local strategy configuration...
));

// Google Strategy
passport.use('google', new GoogleStrategy(
  // Google strategy configuration...
));

// JWT Strategy
passport.use('jwt', new JwtStrategy(
  // JWT strategy configuration...
));

// Common verify functions
async function findOrCreateOAuthUser(provider, profile) {
  // Standardized function to find or create users from OAuth profiles
  try {
    // First, try to find by provider ID
    const providerIdField = `${provider}Id`; // e.g., 'googleId', 'facebookId'
    let user = await User.findOne({ [providerIdField]: profile.id });
    
    // If not found, try to find by email
    if (!user && profile.emails && profile.emails.length) {
      const email = profile.emails[0].value;
      user = await User.findOne({ email });
      
      if (user) {
        // User exists, link this provider
        user[providerIdField] = profile.id;
        await user.save();
      } else {
        // Create new user
        user = await User.create({
          [providerIdField]: profile.id,
          username: profile.displayName || profile.username,
          email: profile.emails[0].value,
          picture: profile.photos?.[0]?.value
        });
      }
    }
    
    return user;
  } catch (err) {
    throw err;
  }
}

// Login routes that specify which strategy to use
app.post('/login', 
  passport.authenticate('local', { 
    successRedirect: '/profile',
    failureRedirect: '/login',
    failureFlash: true
  })
);

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

app.get('/api/profile',
  passport.authenticate(['jwt', 'bearer'], { session: false }),
  (req, res) => {
    res.json(req.user);
  }
);

Strategy Selection Logic

Passport can attempt multiple strategies for a single route:

// Try multiple strategies in order (first one that succeeds wins)
app.get('/api/data',
  passport.authenticate(['jwt', 'apikey'], { session: false }),
  (req, res) => {
    res.json({ data: 'Protected data' });
  }
);

This allows clients to authenticate using either method, providing flexibility while maintaining security.

Strategy Naming and Reuse

// Using named strategies for different configurations

// Regular login strategy
passport.use('local-login', new LocalStrategy(
  {
    usernameField: 'email',
    passwordField: 'password'
  },
  async (email, password, done) => {
    // Login verification logic...
  }
));

// Admin login strategy with additional security
passport.use('local-admin', new LocalStrategy(
  {
    usernameField: 'email',
    passwordField: 'password'
  },
  async (email, password, done) => {
    try {
      const user = await User.findOne({ email });
      
      // Verify password
      if (!user || !(await user.comparePassword(password))) {
        return done(null, false, { message: 'Invalid credentials' });
      }
      
      // Check if user is an admin
      if (user.role !== 'admin') {
        return done(null, false, { message: 'Admin access required' });
      }
      
      // Check IP address or other security factors
      
      return done(null, user);
    } catch (err) {
      return done(err);
    }
  }
));

// Use specific named strategies in routes
app.post('/login',
  passport.authenticate('local-login', { 
    successRedirect: '/profile',
    failureRedirect: '/login'
  })
);

app.post('/admin/login',
  passport.authenticate('local-admin', { 
    successRedirect: '/admin/dashboard',
    failureRedirect: '/admin/login'
  })
);

Advanced Passport Features

Dynamic Strategy Configuration

// Dynamic strategy configuration based on database settings
async function configurePassport() {
  try {
    // Get OAuth providers from database
    const providers = await OAuthProvider.find({ active: true });
    
    // Configure each provider dynamically
    for (const provider of providers) {
      switch (provider.name) {
        case 'google':
          passport.use(new GoogleStrategy({
            clientID: provider.clientId,
            clientSecret: provider.clientSecret,
            callbackURL: provider.callbackURL,
            scope: provider.scope.split(',')
          }, commonOAuthCallback));
          break;
        
        case 'facebook':
          passport.use(new FacebookStrategy({
            clientID: provider.clientId,
            clientSecret: provider.clientSecret,
            callbackURL: provider.callbackURL,
            profileFields: ['id', 'displayName', 'photos', 'email']
          }, commonOAuthCallback));
          break;
        
        // Add more providers as needed
      }
    }
    
    console.log('Passport strategies configured successfully');
  } catch (err) {
    console.error('Error configuring Passport:', err);
  }
}

// Common OAuth callback function
async function commonOAuthCallback(accessToken, refreshToken, profile, done) {
  try {
    // Determine provider from profile.provider (set by Passport)
    const provider = profile.provider; // 'google', 'facebook', etc.
    
    // Use common function to find or create user
    const user = await findOrCreateOAuthUser(provider, profile);
    
    return done(null, user);
  } catch (err) {
    return done(err);
  }
}

// Call the configuration function during app startup
configurePassport();

Custom Authentication Error Handling

// Custom error handling with Passport
app.post('/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      return next(err); // Server error
    }
    
    if (!user) {
      // Authentication failed
      // Instead of redirecting, return a JSON response
      return res.status(401).json({
        success: false,
        message: info.message || 'Authentication failed'
      });
    }
    
    // Log in the user manually
    req.login(user, (loginErr) => {
      if (loginErr) {
        return next(loginErr);
      }
      
      // Success response
      return res.json({
        success: true,
        user: {
          id: user._id,
          username: user.username,
          email: user.email
        }
      });
    });
  })(req, res, next);
});

// API login with detailed error handling
app.post('/api/login', (req, res, next) => {
  passport.authenticate('local', (err, user, info) => {
    if (err) {
      return res.status(500).json({
        status: 'error',
        code: 'server_error',
        message: 'Internal server error',
        details: process.env.NODE_ENV === 'development' ? err.message : undefined
      });
    }
    
    if (!user) {
      // Determine specific error type
      let errorCode = 'authentication_failed';
      let statusCode = 401;
      
      if (info && info.message) {
        if (info.message.includes('not found')) {
          errorCode = 'user_not_found';
        } else if (info.message.includes('password')) {
          errorCode = 'invalid_password';
        } else if (info.message.includes('locked')) {
          errorCode = 'account_locked';
          statusCode = 403;
        }
      }
      
      return res.status(statusCode).json({
        status: 'error',
        code: errorCode,
        message: info.message || 'Authentication failed'
      });
    }
    
    // Generate JWT instead of session
    const token = jwt.sign(
      { sub: user._id },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    return res.json({
      status: 'success',
      data: {
        token,
        user: {
          id: user._id,
          username: user.username,
          email: user.email
        }
      }
    });
  })(req, res, next);
});

Middleware Composition

// Composing multiple middleware for authentication
function rateLimit(max, timeWindow) {
  const requests = {};
  
  return (req, res, next) => {
    const ip = req.ip;
    
    // Initialize or get request count
    requests[ip] = requests[ip] || { count: 0, resetTime: Date.now() + timeWindow };
    
    // Reset counter if time window has passed
    if (Date.now() > requests[ip].resetTime) {
      requests[ip] = { count: 0, resetTime: Date.now() + timeWindow };
    }
    
    // Increment request count
    requests[ip].count++;
    
    // Check if rate limit exceeded
    if (requests[ip].count > max) {
      return res.status(429).json({ message: 'Too many login attempts' });
    }
    
    next();
  };
}

// Apply rate limiting before Passport authentication
app.post('/login',
  rateLimit(5, 60 * 1000), // 5 attempts per minute
  passport.authenticate('local', {
    successRedirect: '/profile',
    failureRedirect: '/login',
    failureFlash: true
  })
);

// More complex middleware composition
app.post('/api/admin/login',
  // Check if admin logins are enabled
  (req, res, next) => {
    if (process.env.DISABLE_ADMIN_LOGIN === 'true') {
      return res.status(503).json({ message: 'Admin login temporarily disabled' });
    }
    next();
  },
  // Apply stricter rate limiting for admin
  rateLimit(3, 5 * 60 * 1000), // 3 attempts per 5 minutes
  // Log all admin login attempts
  (req, res, next) => {
    console.log(`Admin login attempt from ${req.ip} for ${req.body.email}`);
    next();
  },
  // Use custom authentication handling
  (req, res, next) => {
    passport.authenticate('local-admin', (err, user, info) => {
      // Custom auth handling...
    })(req, res, next);
  }
);

Testing Passport.js Applications

Testing authentication is crucial to ensure your security implementation works correctly:

// Testing Passport authentication with Jest and Supertest
const request = require('supertest');
const app = require('../app'); // Your Express app
const User = require('../models/User');
const mongoose = require('mongoose');

// Test database connection
beforeAll(async () => {
  await mongoose.connect(process.env.TEST_MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true
  });
});

// Clean up after tests
afterAll(async () => {
  await mongoose.connection.dropDatabase();
  await mongoose.connection.close();
});

// Clear users before each test
beforeEach(async () => {
  await User.deleteMany({});
});

describe('Authentication API', () => {
  // Test user registration
  test('should register a new user', async () => {
    const response = await request(app)
      .post('/register')
      .send({
        username: 'testuser',
        email: 'test@example.com',
        password: 'password123'
      });
    
    expect(response.status).toBe(302); // Redirect after registration
    expect(response.headers.location).toBe('/login');
    
    // Verify user was created in database
    const user = await User.findOne({ email: 'test@example.com' });
    expect(user).toBeTruthy();
    expect(user.username).toBe('testuser');
  });
  
  // Test login with valid credentials
  test('should log in with valid credentials', async () => {
    // Create a test user
    const password = 'password123';
    const bcrypt = require('bcrypt');
    const hashedPassword = await bcrypt.hash(password, 10);
    
    await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: hashedPassword
    });
    
    // Attempt login
    const response = await request(app)
      .post('/login')
      .send({
        username: 'testuser',
        password: password
      });
    
    expect(response.status).toBe(302); // Redirect after login
    expect(response.headers.location).toBe('/profile');
    
    // Verify session cookie was set
    expect(response.headers['set-cookie']).toBeDefined();
  });
  
  // Test login with invalid credentials
  test('should reject invalid credentials', async () => {
    // Create a test user
    const password = 'password123';
    const bcrypt = require('bcrypt');
    const hashedPassword = await bcrypt.hash(password, 10);
    
    await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: hashedPassword
    });
    
    // Attempt login with wrong password
    const response = await request(app)
      .post('/login')
      .send({
        username: 'testuser',
        password: 'wrongpassword'
      });
    
    expect(response.status).toBe(302); // Redirect after failed login
    expect(response.headers.location).toBe('/login'); // Back to login page
  });
  
  // Test accessing protected route
  test('should access protected route with authentication', async () => {
    // Create a test user
    const user = await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: await bcrypt.hash('password123', 10)
    });
    
    // Login to get cookies
    const loginResponse = await request(app)
      .post('/login')
      .send({
        username: 'testuser',
        password: 'password123'
      });
    
    const cookies = loginResponse.headers['set-cookie'];
    
    // Try accessing protected route with cookie
    const protectedResponse = await request(app)
      .get('/profile')
      .set('Cookie', cookies);
    
    expect(protectedResponse.status).toBe(200);
    expect(protectedResponse.text).toContain('Welcome, testuser');
  });
  
  // Test accessing protected route without authentication
  test('should reject unauthenticated access to protected route', async () => {
    const response = await request(app).get('/profile');
    
    expect(response.status).toBe(302); // Redirect
    expect(response.headers.location).toBe('/login');
  });
});

Testing OAuth Strategies

// Testing OAuth strategies
const passport = require('passport');

// Mock the Google strategy for testing
jest.mock('passport-google-oauth20');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

describe('OAuth Authentication', () => {
  beforeEach(() => {
    // Clear all mocks
    jest.clearAllMocks();
    
    // Mock strategy implementation
    GoogleStrategy.mockImplementation((options, verify) => {
      // Store the verify callback for testing
      this.verifyCallback = verify;
      
      // Return mock strategy instance
      return {
        name: 'google',
        authenticate: jest.fn()
      };
    });
  });
  
  test('should initialize Google strategy with correct options', () => {
    // Import the module that configures passport
    require('../config/passport');
    
    // Check that GoogleStrategy was constructed with correct options
    expect(GoogleStrategy).toHaveBeenCalledWith(
      expect.objectContaining({
        clientID: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        callbackURL: expect.stringContaining('/auth/google/callback')
      }),
      expect.any(Function)
    );
  });
  
  test('should create a new user from Google profile', async () => {
    // Import the module that configures passport
    require('../config/passport');
    
    // Get the verify callback
    const verifyCallback = GoogleStrategy.mock.calls[0][1];
    
    // Create a mock profile
    const profile = {
      id: 'google123',
      displayName: 'Test User',
      emails: [{ value: 'google-user@example.com' }],
      photos: [{ value: 'https://example.com/photo.jpg' }]
    };
    
    // Create mock done callback
    const done = jest.fn();
    
    // Call the verify function
    await verifyCallback('test-token', 'test-refresh-token', profile, done);
    
    // Verify new user was created
    const user = await User.findOne({ googleId: 'google123' });
    expect(user).toBeTruthy();
    expect(user.email).toBe('google-user@example.com');
    
    // Verify done was called with user
    expect(done).toHaveBeenCalledWith(null, expect.objectContaining({
      googleId: 'google123'
    }));
  });
});

Best Practices and Security Considerations

When implementing Passport.js for authentication, keep these best practices in mind:

Security Checklist

  • Use HTTPS for all authentication-related traffic
  • Properly hash passwords (bcrypt, argon2) with appropriate work factors
  • Set secure cookie options (HttpOnly, Secure, SameSite, etc.)
  • Implement rate limiting for login attempts
  • Use short-lived tokens with proper refresh mechanisms
  • Validate and sanitize all user inputs
  • Keep strategy-specific secrets in environment variables
  • Implement account lockout mechanisms after multiple failed attempts
  • Enable multifactor authentication for sensitive operations
  • Regularly audit authentication logs for suspicious activity

Additional Best Practices

// Example of secure session setup
app.use(session({
  secret: process.env.SESSION_SECRET, // Strong, random secret
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production', // HTTPS only in production
    sameSite: 'strict', // Protects against CSRF
    maxAge: 2 * 60 * 60 * 1000 // 2 hours
  },
  store: new RedisStore({
    client: redisClient,
    ttl: 86400 // 24 hours (maximum session lifetime)
  })
}));

// Implement session timeout/expiration check
app.use((req, res, next) => {
  // Skip for non-authenticated requests
  if (!req.isAuthenticated()) {
    return next();
  }
  
  // Check session last active timestamp
  const lastActive = req.session.lastActive || req.session.cookie.originalMaxAge;
  const now = Date.now();
  const idleTimeout = 30 * 60 * 1000; // 30 minutes
  
  if (now - lastActive > idleTimeout) {
    // Session has been idle too long, log out
    req.logout(function(err) {
      if (err) { return next(err); }
      return res.redirect('/login?expired=1');
    });
  } else {
    // Update last active timestamp
    req.session.lastActive = now;
    next();
  }
});

Practice Activities

Activity 1: Basic Authentication System

Build a complete authentication system with Passport.js:

  1. Set up Express with Passport.js and express-session
  2. Implement the Local Strategy for username/password authentication
  3. Create registration, login, and logout functionality
  4. Add password hashing with bcrypt
  5. Implement protected routes that require authentication
  6. Create user profile page that displays user information

Focus on security best practices and proper error handling.

Activity 2: Multi-Provider OAuth

Extend your authentication system with OAuth providers:

  1. Register applications with at least two OAuth providers (Google, GitHub, etc.)
  2. Implement OAuth strategies for each provider
  3. Create a unified user account system that works with all providers
  4. Add account linking functionality to connect multiple providers to one account
  5. Create a user dashboard that shows all connected providers
  6. Implement provider disconnect functionality

Test with multiple user scenarios to ensure a smooth authentication experience.

Activity 3: API Authentication

Create a REST API with JWT authentication:

  1. Implement JWT Strategy for authenticating API requests
  2. Create endpoints for user registration and login that return JWT tokens
  3. Add token refresh functionality for expired tokens
  4. Implement role-based access control for API endpoints
  5. Create a simple frontend that demonstrates JWT authentication
  6. Add logout and token revocation functionality

Focus on creating a stateless, secure API authentication system.

Activity 4: Custom Authentication Strategy

Design and implement a custom Passport strategy:

  1. Define a unique authentication mechanism (API key, magic link, etc.)
  2. Create a custom Passport strategy that implements this mechanism
  3. Implement the necessary database models and storage
  4. Add routes that use your custom strategy
  5. Create an admin interface for managing authentication credentials
  6. Test your strategy with different scenarios

Document your strategy implementation and how it compares to built-in strategies.

Additional Resources

Summary

With Passport.js, you can implement sophisticated authentication systems while keeping your code clean, modular, and maintainable.