Implementing Username and Password Authentication with Passport LocalStrategy
Learn how to implement secure username and password authentication using Passport.js LocalStrategy, including session management and password hashing integration.
05 Oct 2026, 23:39 UTC

The Challenge: Managing User Sessions Manually
Building a custom authentication system often leads to repetitive code for extracting credentials from request bodies, managing session cookies, and verifying user identities across multiple routes. The goal is to decouple the authentication logic (how a user is verified) from the application logic (what the user can access).
The Passport.js LocalStrategy solves this by providing a standardized middleware pattern. It handles the extraction of username and password fields and delegates the verification to a custom callback function, allowing you to switch databases or hashing algorithms without rewriting your route handlers.
Prerequisites
- Node.js environment with Express.js installed.
- A user data store (e.g., MongoDB, PostgreSQL, or a mock JSON array).
bcryptjsorbcryptfor password hashing (Passport does not hash passwords internally).express-sessionfor maintaining user state across requests.
Configuration and Implementation
To implement LocalStrategy, you must configure the strategy, define how users are stored in the session, and protect your routes.
1. Strategy Definition
Run these commands in your project root to install the necessary dependencies:
npm install passport passport-local express-session bcryptjs
Configure the strategy in your main application file. The verify callback is where you interact with your database.
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcryptjs');
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
// 1. Find user in database
const user = await User.findOne({ username: username });
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
// 2. Compare hashed password
const match = await bcrypt.compare(password, user.password);
if (!match) {
return done(null, false, { message: 'Incorrect password.' });
}
// 3. Success: pass user object to passport
return done(null, user);
} catch (err) {
return done(err);
}
}
));
2. Session Serialization
Passport needs to know how to store the user in the session (serialization) and how to retrieve the full user object from the session ID (deserialization).
passport.serializeUser((user, done) => {
// Store only the ID to keep the session cookie small
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
3. Middleware Integration
Initialize the session and passport middleware before your route definitions. Order is critical: session must come before passport.session().
app.use(session({
secret: 'your_secret_key',
resave: false,
saveUninitialized: false
}));
app.use(passport.initialize());
app.use(passport.session());
Applying Authentication to Routes
Use passport.authenticate('local') for the login route and a custom middleware function to protect private pages.
| Route Type | Implementation | Expected Behavior |
|---|---|---|
| Login POST | passport.authenticate('local') |
Redirects to dashboard on success; returns 401 on failure. |
| Protected GET | (req, res, next) => { if (req.isAuthenticated()) return next(); ... } |
Allows access if session is valid; otherwise redirects to login. |
Example Protected Route:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).send('Please log in to access this resource');
}
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.send(`Welcome, ${req.user.username}`);
});
Verification and Diagnostics
To verify the implementation, perform the following checks:
- Credential Test: Send a POST request to
/loginwith valid credentials. Check the response headers for aSet-Cookieheader containing the session ID. - Persistence Test: Access
/dashboard. Ifreq.useris populated, thedeserializeUserfunction is working correctly. - Unauthorized Test: Clear browser cookies and attempt to access
/dashboard. The server should return a 401 Unauthorized or a redirect.
Limitations and Risks
- Memory Leaks: The default
express-sessionstore is MemoryStore, which leaks memory in production. Use a Redis or MongoDB store for production environments. - Hanging Requests: If the
done()callback is not called in the LocalStrategy verify function (e.g., inside acatchblock that doesn't return), the client request will hang until it times out. - Plaintext Risk: Never store passwords in plaintext. Always use
bcrypt.hash()during user registration before saving to the database.
Rollback Procedure
Since this implementation changes the application state via middleware and session cookies, to revert these changes:
- Remove the
passport.use,serializeUser, anddeserializeUserblocks. - Remove
passport.initialize()andpassport.session()from the middleware stack. - Delete the session cookie from the client browser to clear existing authenticated states.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.