Express Passport Integration
Introduction
Authentication is a crucial aspect of web applications, allowing you to verify user identities and provide personalized experiences. In Express.js applications, Passport.js has emerged as the go-to authentication middleware due to its flexibility and extensive ecosystem of authentication strategies.
Passport is modular, which means you can plug in various authentication mechanisms (called "strategies") without changing your application code. Whether you need local authentication with username and password, or third-party authentication via OAuth providers like Google, Facebook, or Twitter, Passport handles it all with a consistent API.
In this guide, we'll learn how to integrate Passport.js into an Express application and implement common authentication patterns.
Prerequisites
Before we begin, make sure you have:
- Basic understanding of Express.js
- Node.js installed on your system
- npm (Node Package Manager)
- Familiarity with HTTP concepts and middleware
Installing Required Packages
Let's start by installing the necessary packages:
npm install express express-session passport passport-local mongoose bcryptjs
Here's what each package does:
express
: Web framework for Node.jsexpress-session
: Session middleware for Expresspassport
: Authentication middleware for Node.jspassport-local
: Passport strategy for username/password authenticationmongoose
: MongoDB object modeling toolbcryptjs
: Library for hashing passwords
Setting Up the Express Application
First, let's create a basic Express application structure:
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
// Initialize Express app
const app = express();
// Configure middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Setup session
app.use(session({
secret: 'your-secret-key', // Used to sign the session ID cookie
resave: false, // Don't save session if unmodified
saveUninitialized: false, // Don't create session until something stored
cookie: {
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Start the server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creating a User Model
For authentication, we need to store user information. Let's create a simple User model using Mongoose:
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
// Hash password before saving
UserSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// Method to verify password
UserSchema.methods.verifyPassword = async function(candidatePassword) {
return await bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', UserSchema);
Configuring Passport.js
Now, let's configure Passport with the Local Strategy:
// config/passport.js
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User');
// Local Strategy configuration
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
// Find the user
const user = await User.findOne({ username });
// If user doesn't exist
if (!user) {
return done(null, false, { message: 'Incorrect username' });
}
// Check password
const isMatch = await user.verifyPassword(password);
if (!isMatch) {
return done(null, false, { message: 'Incorrect password' });
}
// Authentication successful
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// Serialize user for the session
passport.serializeUser((user, done) => {
done(null, user.id);
});
// Deserialize user from the session
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
module.exports = passport;
Then, import this configuration in your main app:
// In your main app.js file
require('./config/passport');
Implementing Authentication Routes
Now, let's create routes for registration, login, and logout:
// routes/auth.js
const express = require('express');
const passport = require('passport');
const User = require('../models/User');
const router = express.Router();
// Register route
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user already exists
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
if (existingUser) {
return res.status(400).json({ message: 'User already exists' });
}
// Create new user
const newUser = new User({ username, email, password });
await newUser.save();
res.status(201).json({ message: 'User registered successfully' });
} catch (error) {
res.status(500).json({ message: 'Server error', error: error.message });
}
});
// Login route
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) {
return res.status(401).json({ message: info.message });
}
req.logIn(user, (err) => {
if (err) return next(err);
return res.json({ message: 'Login successful', user: { id: user._id, username: user.username, email: user.email } });
});
})(req, res, next);
});
// Logout route
router.get('/logout', (req, res) => {
req.logout(function(err) {
if (err) { return next(err); }
res.json({ message: 'Logout successful' });
});
});
// Get current user
router.get('/current-user', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: 'Not authenticated' });
}
res.json({
user: {
id: req.user._id,
username: req.user.username,
email: req.user.email
}
});
});
module.exports = router;
Register these routes in your main application:
// In your main app.js file
const authRoutes = require('./routes/auth');
app.use('/api/auth', authRoutes);
Protecting Routes with Authentication Middleware
To restrict access to certain routes for authenticated users only, let's create a middleware:
// middleware/auth.js
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({ message: 'Authentication required' });
}
module.exports = { isAuthenticated };
Now you can use this middleware to protect routes:
// routes/protected.js
const express = require('express');
const { isAuthenticated } = require('../middleware/auth');
const router = express.Router();
router.get('/profile', isAuthenticated, (req, res) => {
res.json({
message: 'You have access to this protected route',
user: {
id: req.user._id,
username: req.user.username,
email: req.user.email
}
});
});
module.exports = router;
Register these protected routes in your main app:
// In your main app.js file
const protectedRoutes = require('./routes/protected');
app.use('/api', protectedRoutes);
Implementing Social Authentication
One of Passport's strengths is its vast array of authentication strategies. Let's implement Google OAuth2 authentication:
First, install the required package:
npm install passport-google-oauth20
Configure the Google strategy:
// In config/passport.js
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: 'YOUR_GOOGLE_CLIENT_ID',
clientSecret: 'YOUR_GOOGLE_CLIENT_SECRET',
callbackURL: "http://localhost:3000/api/auth/google/callback"
},
async (accessToken, refreshToken, profile, done) => {
try {
// Check if user already exists
let user = await User.findOne({ googleId: profile.id });
if (user) {
return done(null, user);
}
// Create new user from Google profile
user = new User({
googleId: profile.id,
username: profile.displayName,
email: profile.emails[0].value
});
await user.save();
return done(null, user);
} catch (err) {
return done(err);
}
}
));
Add Google authentication routes:
// In routes/auth.js
router.get('/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Successful authentication
res.redirect('/dashboard');
}
);
Real-World Example: Complete Authentication System
Let's build a more complete example that ties together all the concepts:
// app.js
const express = require('express');
const session = require('express-session');
const mongoose = require('mongoose');
const passport = require('passport');
const path = require('path');
// Initialize app
const app = express();
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/auth_demo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log('MongoDB connected'))
.catch(err => console.log('MongoDB connection error:', err));
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// Session configuration
app.use(session({
secret: 'your_secret_key',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 } // 24 hours
}));
// Passport initialization
app.use(passport.initialize());
app.use(passport.session());
// Import Passport config
require('./config/passport');
// Routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api', require('./routes/protected'));
// Home route
app.get('/', (req, res) => {
res.send(`
<h1>Welcome to Authentication Demo</h1>
${req.isAuthenticated() ?
`<p>Hello, ${req.user.username}! <a href="/api/auth/logout">Logout</a></p>
<p><a href="/api/profile">View Profile</a></p>` :
`<p><a href="/login.html">Login</a> | <a href="/register.html">Register</a></p>
<p><a href="/api/auth/google">Login with Google</a></p>`
}
`);
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong!', error: err.message });
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Create simple HTML forms for login and registration:
<!-- public/login.html -->
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/api/auth/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username" required />
</div>
<div>
<label>Password:</label>
<input type="password" name="password" required />
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
<p>Don't have an account? <a href="/register.html">Register</a></p>
<p>Or <a href="/api/auth/google">Login with Google</a></p>
</body>
</html>
<!-- public/register.html -->
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
</head>
<body>
<h1>Register</h1>
<form action="/api/auth/register" method="post">
<div>
<label>Username:</label>
<input type="text" name="username" required />
</div>
<div>
<label>Email:</label>
<input type="email" name="email" required />
</div>
<div>
<label>Password:</label>
<input type="password" name="password" required />
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
<p>Already have an account? <a href="/login.html">Login</a></p>
</body>
</html>
Best Practices for Production
When deploying your Passport-integrated Express application to production, consider these best practices:
- Use environment variables for sensitive information like session secrets and API keys:
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
- Set secure cookies in production:
app.use(session({
// ... other options
cookie: {
secure: process.env.NODE_ENV === 'production', // Only use cookies over HTTPS
httpOnly: true, // Prevents JavaScript access to the cookie
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
- Use a production-ready session store instead of the in-memory store:
npm install connect-mongo
const MongoStore = require('connect-mongo');
app.use(session({
// ... other options
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI
})
}));
- Implement rate limiting to prevent brute force attacks:
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 login attempts per window
message: "Too many login attempts, please try again after 15 minutes"
});
// Apply to login route
app.use('/api/auth/login', loginLimiter);
Summary
In this guide, we've learned how to integrate Passport.js with Express to create a flexible authentication system. We covered:
- Setting up an Express application with Passport and session support
- Implementing local authentication with username and password
- Creating user registration, login, and logout routes
- Protecting routes with authentication middleware
- Adding social authentication with Google OAuth
- Building a complete authentication system with frontend forms
- Implementing production best practices
Passport's strength lies in its flexibility and vast plugin ecosystem. The consistent API across different authentication strategies makes it easy to add various login methods to your application without significant code changes.
Further Resources and Exercises
Resources
Exercises
- Add another authentication strategy such as Twitter, Facebook, or GitHub OAuth.
- Implement email verification for new user registrations.
- Create a password reset flow with token-based verification.
- Add multi-factor authentication using time-based one-time passwords (TOTP).
- Create a role-based authorization system to restrict access based on user roles.
By completing these exercises, you'll gain a deeper understanding of authentication concepts and how to implement them in your Express applications using Passport.js.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)