Back to Scaling Node.js Applications guides

Integrating Passport.js with Express

Stanley Ulili
Updated on July 29, 2025

Passport.js is the most popular authentication middleware for Node.js, supporting over 500 strategies with flexible, modular design.

It simplifies the implementation of both complex and straightforward authentication flows, ranging from local logins to OAuth with social providers.

This guide covers configuring multiple strategies, securing routes, and best practices to build robust, user-friendly authentication systems in your Express.js app.

Prerequisites

Before diving into the implementation details, make sure you have a recent version of Node.js and npm installed on your development machine. This guide assumes you are familiar with the basics of Express.js and fundamental web authentication concepts, such as sessions, cookies, and HTTP status codes.

Step 1 — Creating a basic Express server

Building secure authentication requires starting with a solid foundation. You'll begin with a minimal Express server and progressively add authentication capabilities.

Create your project directory and navigate into it:

 
mkdir passport-express-auth && cd passport-express-auth

Initialize your Node.js project:

 
npm init -y

Configure your project to use ES modules:

 
npm pkg set type="module"

Install Express to get started:

 
npm install express

Create a basic server file called app.js:

app.js
import express from 'express';

const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello World - Authentication Server' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Set up your development script for automatic restarts:

 
npm pkg set scripts.dev="node --watch app.js"

Start your server:

 
npm run dev

You should see:

Output
Server running on http://localhost:3000

Now visit http://localhost:3000 in the browser of your choice.

You should see the JSON response with your "Hello World" message, confirming that Express is working correctly.

Basic Express server response showing Hello World JSON message

This basic setup provides us with a working Express server that can respond with JSON. The server runs on port 3000 and handles GET requests with a simple message, serving as the foundation for building our authentication system.

Step 2 — Adding essential middleware

Authentication systems require several middleware components for request parsing, logging, and handling cross-origin requests. You'll add these foundational pieces before implementing Passport.js to ensure your server can properly handle authentication requests.

Install the additional middleware packages:

 
npm install morgan cors

These middleware components serve specific purposes in your authentication system:

  • morgan provides HTTP request logging for monitoring and debugging
  • cors enables Cross-Origin Resource Sharing for API accessibility

Update your app.js to include the essential middleware:

app.js
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
const app = express();
// Essential middleware
app.use(morgan('dev'));
app.use(cors({
origin: 'http://localhost:3000',
credentials: true
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => { res.json({ message: 'Hello World - Authentication Server' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });

In the updated code, you are building a strong middleware foundation for authentication.

Morgan logging displays all incoming requests in your terminal, making it significantly easier to debug authentication flows as they occur.

The CORS configuration allows credentials to be sent explicitly across different origins, which becomes essential when implementing session-based authentication.

Additionally, you're enabling both JSON and URL-encoded request parsing, ensuring your server can handle authentication requests whether they come as JSON payloads from API clients or form submissions from web interfaces.

Save your changes, and you'll notice Morgan now logs each request to your terminal. Visit http://localhost:3000 again and you should see a log entry like:

Output
Server running on http://localhost:3000
GET / 200 2.141 ms - 49

This confirms your middleware stack is configured correctly and ready for authentication integration.

Step 3 — Installing Passport.js, sessions, and creating user storage

Authentication systems require three foundational components working together: session management for maintaining user state, Passport.js for handling authentication strategies, and a user storage system. You'll set up all three components in this step to create a cohesive authentication foundation.

Install all the required dependencies for authentication and database functionality:

 
npm install express-session passport passport-local sequelize sqlite3 bcrypt uuid

These packages work together to provide complete authentication capabilities. Express-session maintains user sessions across requests, while passport and passport-local handle the authentication logic.

Sequelize and sqlite3 provide database functionality, bcrypt ensures secure password hashing, and uuid generates unique user identifiers.

Add session configuration and Passport initialization to your server:

app.js
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import session from 'express-session';
import passport from 'passport';
const app = express(); // Essential middleware app.use(morgan('dev')); app.use(cors({ origin: 'http://localhost:3000', credentials: true })); app.use(express.json()); app.use(express.urlencoded({ extended: true }));
// Session configuration
app.use(session({
secret: 'your-secret-key-change-in-production',
resave: false,
saveUninitialized: false,
cookie: {
secure: false, // Set to true in production with HTTPS
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
app.get('/', (req, res) => { res.json({ message: 'Hello World - Authentication Server',
authenticated: req.isAuthenticated ? req.isAuthenticated() : false
}); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on http://localhost:${PORT}`); });

In this setup, you're configuring the session middleware to create secure user sessions with optimized settings.

The resave: false and saveUninitialized: false options prevent unnecessary session writes, boosting performance. Passport initialization sets up the authentication framework and integrates seamlessly with your existing session system.

You've also updated the root route to display authentication status using Passport's req.isAuthenticated() method.

Now create the directory structure for your database and models:

 
mkdir -p config models data

Create your database configuration file:

config/database.js
import { Sequelize } from 'sequelize';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const dbPath = path.join(__dirname, '..', 'data', 'auth.db');

const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: dbPath,
  logging: false
});

export default sequelize;

This configuration establishes your SQLite database connection, creating the database file in a data directory and disabling query logging for cleaner output.

Create a secure User model with automatic password hashing:

models/user.js
import { DataTypes } from 'sequelize';
import { v4 as uuidv4 } from 'uuid';
import bcrypt from 'bcrypt';
import sequelize from '../config/database.js';

const User = sequelize.define('User', {
  id: {
    type: DataTypes.UUID,
    defaultValue: () => uuidv4(),
    primaryKey: true,
  },
  username: {
    type: DataTypes.STRING(50),
    allowNull: false,
    unique: true
  },
  password: {
    type: DataTypes.STRING(255),
    allowNull: false
  }
}, {
  tableName: 'users',
  timestamps: true,
  hooks: {
    beforeCreate: async (user) => {
      if (user.password) {
        const saltRounds = 12;
        user.password = await bcrypt.hash(user.password, saltRounds);
      }
    }
  }
});

// Add password validation method
User.prototype.validatePassword = async function(password) {
  return await bcrypt.compare(password, this.password);
};

export default User;

This model defines your user table with UUID primary keys, automatic password hashing using bcrypt hooks, and a secure password validation method for authentication.

The User model includes automatic password hashing through Sequelize hooks, ensuring passwords are never stored in plain text. The validatePassword method provides secure password comparison using bcrypt's built-in timing-safe comparison.

Finally, integrate the database with your Express application:

app.js
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import session from 'express-session';
import passport from 'passport';
import sequelize from './config/database.js';
import User from './models/user.js';
const app = express(); ... // Initialize Passport app.use(passport.initialize()); app.use(passport.session());
// Database initialization
async function initDatabase() {
try {
await sequelize.sync();
console.log('Database synchronized');
} catch (error) {
console.error('Database error:', error);
}
}
app.get('/', (req, res) => { res.json({ message: 'Hello World - Authentication Server', authenticated: req.isAuthenticated ? req.isAuthenticated() : false }); }); const PORT = process.env.PORT || 3000;
app.listen(PORT, async () => {
console.log(`Server running on http://localhost:${PORT}`);
await initDatabase();
});

This updated app imports your database configuration and User model, then initializes the database when the server starts, creating tables automatically through Sequelize's sync method.

Save your changes and you should see the complete initialization:

Output
Server running on http://localhost:3000
Database synchronized

Your authentication foundation is now complete with sessions, Passport integration, and secure user storage all working together.

Visit http://localhost:3000 to confirm everything is working. You should see the authentication status in the response:

Screenshot of the browser

Output
{"message":"Hello World - Authentication Server","authenticated":false}

The authenticated: false status is expected since you haven't implemented login functionality yet. This confirms that your Express server, sessions, Passport.js initialization, and database synchronization are all working correctly together.

Step 4 — Configuring Passport local strategy

With your database and Passport foundation ready, you need to configure Passport's Local Strategy to handle username/password authentication. This involves telling Passport how to verify credentials and manage user sessions.

Create a configuration file for your Passport strategies:

config/passport.js
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import User from '../models/user.js';

// Configure Local Strategy
passport.use(new LocalStrategy(
  {
    usernameField: 'username',
    passwordField: 'password'
  },
  async (username, password, done) => {
    try {
      const user = await User.findOne({ where: { username } });

      if (!user) {
        return done(null, false, { message: 'Invalid username or password' });
      }

      const isValid = await user.validatePassword(password);
      if (!isValid) {
        return done(null, false, { message: 'Invalid username or password' });
      }

      return done(null, user);
    } catch (error) {
      return done(error);
    }
  }
));

export default passport;

This strategy configuration defines how Passport verifies user credentials. It looks up users by username and uses the validatePassword method you created earlier for secure password comparison.

Now add session serialization to handle user sessions:

config/passport.js
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import User from '../models/user.js';

// Configure Local Strategy
passport.use(new LocalStrategy(
  ...
));

// Serialize user for session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize user from session
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findByPk(id);
done(null, user);
} catch (error) {
done(error);
}
});
export default passport;

Session serialization stores only the user ID in the session for efficiency, while deserialization retrieves the full user object when needed. This keeps sessions lightweight while maintaining complete user access throughout your application.

Import this configuration in your main app:

app.js
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import session from 'express-session';
import passport from 'passport';
import sequelize from './config/database.js';
import User from './models/user.js';
import './config/passport.js';
// ... rest of your app remains the same

This import ensures your Passport strategy configuration is loaded when the application starts, making the Local Strategy available for authentication requests.

Save your changes and restart your server. You should see the same output as before, but now Passport is configured with your Local Strategy and ready to authenticate users:

Output
Server running on http://localhost:3000
Database synchronized

Your Passport configuration is now complete and ready to handle user authentication. In the next step, you'll create the authentication routes that users will interact with.

Step 5 — Creating authentication routes

Now that Passport is configured with your Local Strategy, you'll create the actual authentication endpoints that users will interact with. You'll start with registration and login routes to handle user account creation and authentication.

Create a routes directory and authentication router:

 
mkdir routes
routes/auth.js
import express from 'express';
import passport from 'passport';
import User from '../models/user.js';

const router = express.Router();

export default router;

This creates the foundation for your authentication routes using Express Router, which allows you to organize routes in separate modules.

Now add the registration endpoint:

routes/auth.js
import express from 'express';
import passport from 'passport';
import User from '../models/user.js';

const router = express.Router();

// Registration endpoint
router.post('/register', async (req, res) => {
try {
const { username, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ where: { username } });
if (existingUser) {
return res.status(400).json({ error: 'User already exists' });
}
// Create new user (password will be hashed automatically)
const user = await User.create({ username, password });
res.status(201).json({
message: 'User created successfully',
user: { id: user.id, username: user.username }
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({ error: 'Registration failed' });
}
});
export default router;

This registration endpoint creates new users while preventing duplicates and automatically hashing passwords through your Sequelize hook. The response excludes the password for security reasons.

Add the login endpoint that uses your Passport Local Strategy:

routes/auth.js
import express from 'express';
import passport from 'passport';
import User from '../models/user.js';

const router = express.Router();

// Registration endpoint
router.post('/register', async (req, res) => {
  try {
    const { username, password } = req.body;

    const existingUser = await User.findOne({ where: { username } });
    if (existingUser) {
      return res.status(400).json({ error: 'User already exists' });
    }

    const user = await User.create({ username, password });

    res.status(201).json({ 
      message: 'User created successfully',
      user: { id: user.id, username: user.username }
    });
  } catch (error) {
    console.error('Registration error:', error);
    res.status(500).json({ error: 'Registration failed' });
  }
});

// Login endpoint
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return res.status(500).json({ error: 'Authentication error' });
}
if (!user) {
return res.status(401).json({ error: info.message || 'Login failed' });
}
req.logIn(user, (err) => {
if (err) {
return res.status(500).json({ error: 'Login error' });
}
res.json({
message: 'Login successful',
user: { id: user.id, username: user.username }
});
});
})(req, res, next);
});
export default router;

The login endpoint uses Passport's authenticate method with a custom callback, giving you complete control over the authentication response format and error handling.

Connect your authentication routes to the main application:

app.js
import express from 'express';
import morgan from 'morgan';
import cors from 'cors';
import session from 'express-session';
import passport from 'passport';
import sequelize from './config/database.js';
import User from './models/user.js';
import './config/passport.js';
import authRoutes from './routes/auth.js';
const app = express(); ... // Initialize Passport app.use(passport.initialize()); app.use(passport.session());
// Routes
app.use('/auth', authRoutes);
// Database initialization async function initDatabase() { try { await sequelize.sync(); console.log('Database synchronized'); } catch (error) { console.error('Database error:', error); } } ...

This mounts your authentication routes under the /auth path, making them accessible at /auth/register and /auth/login.

Save your changes, and you should see the familiar server startup:

Output
Server running on http://localhost:3000
Database synchronized

Your authentication routes are now ready for testing. In the next step, you'll test the registration and login functionality using command-line tools.

Step 6 — Testing user registration and login

With your authentication routes in place, you can now test the complete registration and login functionality. You'll use curl commands to simulate client requests, though you can also use Postman or any other API testing tool for a more visual experience.

First, test user registration by creating a new account:

 
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"testpass123"}' \
  -s | jq

You should receive a successful registration response:

Output
{
  "message": "User created successfully",
  "user": {
    "id": "50c45f88-2934-4ff3-94d0-2db9be0a19ba",
    "username": "testuser"
}

If you prefer using Postman, create a POST request to http://localhost:3000/auth/register with the same JSON body but a different username:

Postman registration request showing POST method, URL, and JSON body with username and password

The response confirms that your user was created successfully with a unique UUID and that the password was automatically hashed before storage.

Now test the login functionality using the credentials you just created:

 
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"testpass123"}' \
  -c cookies.txt \
  -s | jq

The -c cookies.txt flag saves the session cookie for subsequent requests. You should see a successful login response:

Output
{
  "message": "Login successful",
  "user": {
    "id": "50c45f88-2934-4ff3-94d0-2db9be0a19ba",
    "username": "testuser"
}

This confirms that your Passport Local Strategy successfully authenticated the user and created a session.

Test the authentication status by visiting the root route with your saved session cookie:

 
curl http://localhost:3000/ \
  -b cookies.txt \
  -s | jq

You should now see that the authentication status has changed:

Output
{
  "message": "Hello World - Authentication Server",
  "authenticated": true
}

The authenticated: true status confirms that your session is working correctly and that Passport recognizes the logged-in user.

Now test error handling by attempting to register a duplicate user:

 
curl -X POST http://localhost:3000/auth/register \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"anotherpass"}' \
  -s | jq

You should receive an error response:

Output
{
  "error": "User already exists"
}

Test invalid login credentials to verify authentication security:

 
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"wrongpassword"}' \
  -s | jq

You should see an authentication failure:

Output
{
  "error": "Invalid username or password"
}

These tests confirm that your registration prevents duplicates, your login authentication works correctly with valid credentials, sessions are maintained properly, and invalid credentials are rejected securely.

Whether you use curl or Postman, your authentication system is now fully functional and ready for additional features.

Step 7 — Adding logout and protected routes

Complete your authentication system by implementing logout functionality and demonstrating how to protect routes that require user authentication. These features round out the core authentication workflow and show how to secure different parts of your application.

Add logout and profile routes to your authentication router:

routes/auth.js
// ... existing imports and routes

// Login endpoint
router.post('/login', (req, res, next) => {
  // ... existing login code
});

// Logout endpoint
router.post('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout error' });
}
res.json({ message: 'Logout successful' });
});
});
// Profile endpoint (protected route example)
router.get('/profile', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({
message: 'Profile data',
user: { id: req.user.id, username: req.user.username }
});
});
export default router;

The logout endpoint uses Passport's logout method to destroy the user session and clear authentication state. The profile endpoint demonstrates route protection by checking authentication status before returning sensitive data.

Test the logout functionality with your existing session:

 
curl -X POST http://localhost:3000/auth/logout \
  -b cookies.txt \
  -s | jq

You should see a successful logout response:

Output
{
  "message": "Logout successful"
}

Verify that the session has been destroyed by checking the authentication status:

 
curl http://localhost:3000/ \
  -b cookies.txt \
  -s | jq

The authentication status should now be false:

Output
{
  "message": "Hello World - Authentication Server",
  "authenticated": false
}

Test the protected profile route without authentication to see the protection in action:

 
curl http://localhost:3000/auth/profile \
  -s | jq

You should receive an authentication error:

Output
{
  "error": "Not authenticated"
}

Now log back in and test the protected route with valid authentication:

 
curl -X POST http://localhost:3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"testuser","password":"testpass123"}' \
  -c cookies.txt \
  -s | jq

Access the profile route with your authenticated session:

 
curl http://localhost:3000/auth/profile \
  -b cookies.txt \
  -s | jq

You should now see the protected profile data:

Output
{
  "message": "Profile data",
  "user": {
    "id": "50c45f88-2934-4ff3-94d0-2db9be0a19ba",
    "username": "testuser"
  }
}

This demonstrates how to implement route-level authentication protection in your Express application. The pattern of checking req.isAuthenticated() can be applied to any route that requires user authentication, ensuring that only logged-in users can access protected resources.

Your authentication system now includes complete user registration, login, logout, and route protection functionality.

Final thoughts

You've successfully built a complete authentication system using Passport.js and Express with secure password hashing, session management, protected routes, and comprehensive error handling.

The modular structure makes it easy to extend with additional features like password reset, email verification, or OAuth strategies. Explore the official Passport.js documentation and strategy packages to learn about additional authentication methods.

For production, consider implementing rate limiting, HTTPS enforcement, secure session storage, and comprehensive logging. Your authentication foundation is now ready to secure real-world applications.

Got an article suggestion? Let us know
Next article
Rate Limiting in Express.js
Complete guide to Express.js rate limiting: protect your API from abuse with tiered limits, authentication guards, and production-ready security patterns.
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github