I’ve been thinking a lot about authentication lately. It’s the gateway to our applications, yet so many implementations feel either overly simplistic or needlessly complex. That’s why I want to walk you through building a robust system using Passport.js, JWT, and Redis—technologies that, when combined properly, create a secure and scalable foundation for any Node.js application.
Getting started requires careful setup. I always begin with the core dependencies, ensuring each package serves a specific purpose in our authentication flow. Here’s what my package.json typically includes for such projects:
const express = require('express');
const passport = require('passport');
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
Why use Redis for sessions, you might ask? Traditional memory stores don’t scale well and lose data when your server restarts. Redis solves both problems while providing lightning-fast access to session data.
Configuring Passport.js involves setting up strategies. The local strategy handles email/password logins, while JWT manages token-based authentication. Here’s how I typically structure the local strategy:
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
const user = await User.findOne({ username });
if (!user) return done(null, false);
const isValid = await bcrypt.compare(password, user.password);
return isValid ? done(null, user) : done(null, false);
} catch (error) {
return done(error);
}
}
));
JWT implementation requires careful token management. I use access tokens for short-term authentication and refresh tokens for maintaining sessions. The refresh tokens get stored in Redis with expiration times, while access tokens remain with the client. Ever wondered what happens if a token gets compromised? That’s where token blacklisting in Redis becomes crucial.
Here’s how I handle token generation:
const generateTokens = (user) => {
const accessToken = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: user.id, tokenVersion: user.tokenVersion },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
};
Session management with Redis involves setting up the store properly. I configure it with sensible defaults for security and performance:
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
What about protecting routes? I create middleware that checks both sessions and JWT tokens, giving flexibility to the frontend on how to authenticate. This approach supports traditional web apps and SPAs equally well.
Error handling deserves special attention. I implement comprehensive logging and monitoring for authentication attempts, especially failed logins. This helps detect potential security threats early.
The complete system comes together through careful integration of these components. Each part plays a specific role: Passport handles authentication logic, JWT provides stateless tokens, and Redis ensures persistent session storage. The result is a system that scales well, maintains security, and provides excellent user experience.
Building this myself taught me that good authentication isn’t just about checking credentials—it’s about creating a seamless, secure experience for users while protecting your application. The balance between security and usability is delicate but achievable with the right tools and approach.
I’d love to hear your thoughts on this approach. Have you implemented similar systems? What challenges did you face? Share your experiences in the comments below, and if you found this helpful, please like and share with others who might benefit from it.