Passport.js Complete Guide | Authentication Middleware for Node.js
이 글의 핵심
Passport.js is authentication middleware for Node.js. It supports 500+ authentication strategies including local, OAuth, and OpenID, making it the standard for authentication.
Introduction
Passport.js is authentication middleware for Node.js. It’s extremely flexible and modular, supporting authentication via username/password, OAuth (Google, Facebook, Twitter), and 500+ other strategies.
Why Passport?
Manual authentication (complex):
// Sessions, cookies, OAuth flows, password hashing...
// Hundreds of lines of code per provider
With Passport:
passport.use(new LocalStrategy(verify));
app.post('/login', passport.authenticate('local'));
1. Installation
npm install passport passport-local express-session
2. Local Strategy (Username/Password)
Setup
const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const bcrypt = require('bcrypt');
const app = express();
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Session configuration
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 24 hours
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
}
}));
// Initialize Passport
app.use(passport.initialize());
app.use(passport.session());
// Configure Local Strategy
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
// Find user
const user = await db.users.findOne({ username });
if (!user) {
return done(null, false, { message: 'Incorrect username' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return done(null, false, { message: 'Incorrect password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// 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 db.users.findById(id);
done(null, user);
} catch (error) {
done(error);
}
});
Login Route
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureFlash: true,
}));
// Or with callback
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
return res.status(401).json({ error: info.message });
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
return res.json({
message: 'Login successful',
user: { id: user.id, username: user.username }
});
});
})(req, res, next);
});
Registration
app.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body;
// Check if user exists
const existing = await db.users.findOne({ username });
if (existing) {
return res.status(409).json({ error: 'Username already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const user = await db.users.create({
username,
email,
password: hashedPassword,
});
// Auto-login after registration
req.login(user, (err) => {
if (err) {
return res.status(500).json({ error: 'Login failed' });
}
res.status(201).json({
message: 'Registration successful',
user: { id: user.id, username: user.username }
});
});
} catch (error) {
res.status(500).json({ error: 'Registration failed' });
}
});
Logout
app.post('/logout', (req, res) => {
req.logout((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.json({ message: 'Logged out successfully' });
});
});
3. Protected Routes
// Middleware to check authentication
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.status(401).json({ error: 'Not authenticated' });
}
// Protected route
app.get('/dashboard', ensureAuthenticated, (req, res) => {
res.json({
message: 'Welcome to dashboard',
user: req.user,
});
});
// Optional authentication
app.get('/profile/:id', (req, res) => {
if (req.isAuthenticated()) {
// Show full profile
res.json({ profile: 'full', user: req.user });
} else {
// Show public profile
res.json({ profile: 'public' });
}
});
4. Google OAuth
npm install passport-google-oauth20
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
},
async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await db.users.findOne({ googleId: profile.id });
if (!user) {
user = await db.users.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
});
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
5. GitHub OAuth
npm install passport-github2
const GitHubStrategy = require('passport-github2').Strategy;
passport.use(new GitHubStrategy({
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
callbackURL: '/auth/github/callback',
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await db.users.findOne({ githubId: profile.id });
if (!user) {
user = await db.users.create({
githubId: profile.id,
username: profile.username,
name: profile.displayName,
avatar: profile.photos[0].value,
});
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// Routes
app.get('/auth/github',
passport.authenticate('github', { scope: ['user:email'] })
);
app.get('/auth/github/callback',
passport.authenticate('github', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/dashboard');
}
);
6. JWT Strategy
npm install passport-jwt jsonwebtoken
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const jwt = require('jsonwebtoken');
// Configure JWT Strategy
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET,
},
async (payload, done) => {
try {
const user = await db.users.findById(payload.sub);
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}
));
// Login route (generate JWT)
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
const user = await db.users.findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT
const token = jwt.sign(
{ sub: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({ token, user: { id: user.id, username: user.username } });
});
// Protected route
app.get('/api/profile',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json({ user: req.user });
}
);
7. Multiple Strategies
// Local + Google + GitHub
passport.use('local', new LocalStrategy(/* ... */));
passport.use('google', new GoogleStrategy(/* ... */));
passport.use('github', new GitHubStrategy(/* ... */));
// Login with local
app.post('/login', passport.authenticate('local'));
// Login with Google
app.get('/auth/google', passport.authenticate('google'));
// Login with GitHub
app.get('/auth/github', passport.authenticate('github'));
8. Custom Callback
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return res.status(500).json({ error: 'Internal 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: 'Session error' });
}
// Custom response
return res.json({
success: true,
user: {
id: user.id,
username: user.username,
email: user.email,
},
token: generateToken(user), // If using JWT
});
});
})(req, res, next);
});
9. Session Store (Production)
With Redis
npm install connect-redis redis
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
// Create Redis client
const redisClient = createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
});
redisClient.connect();
// Configure session with Redis
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
}
}));
10. Remember Me
// Login with remember me
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err || !user) {
return res.status(401).json({ error: 'Login failed' });
}
req.logIn(user, (err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
// Set longer expiry for remember me
if (req.body.rememberMe) {
req.session.cookie.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
}
res.json({ success: true, user });
});
})(req, res, next);
});
11. Real-World Example
const express = require('express');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const session = require('express-session');
const bcrypt = require('bcrypt');
const app = express();
// Middleware
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
}));
app.use(passport.initialize());
app.use(passport.session());
// Local Strategy
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await db.users.findOne({ email });
if (!user) {
return done(null, false, { message: 'User not found' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return done(null, false, { message: 'Invalid password' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
try {
const user = await db.users.findById(id);
done(null, user);
} catch (error) {
done(error);
}
});
// Routes
app.post('/api/register', async (req, res) => {
const { email, password, name } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await db.users.create({
email,
password: hashedPassword,
name,
});
req.login(user, (err) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ user: { id: user.id, email: user.email, name: user.name } });
});
});
app.post('/api/login', passport.authenticate('local'), (req, res) => {
res.json({ user: req.user });
});
app.post('/api/logout', (req, res) => {
req.logout(() => {
res.json({ message: 'Logged out' });
});
});
app.get('/api/me', (req, res) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Not authenticated' });
}
res.json({ user: req.user });
});
app.listen(3000);
12. Best Practices
1. Secure Session Configuration
app.use(session({
secret: process.env.SESSION_SECRET, // Use env var
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000,
httpOnly: true, // Prevent XSS
secure: true, // HTTPS only in production
sameSite: 'strict', // CSRF protection
}
}));
2. Rate Limit Login Attempts
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many login attempts',
});
app.post('/login', loginLimiter, passport.authenticate('local'));
3. Use HTTPS in Production
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (!req.secure) {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});
}
Summary
Passport.js provides flexible authentication:
- 500+ strategies - local, OAuth, OpenID
- Session management built-in
- Easy to extend with custom strategies
- Works with JWT for APIs
- Production-ready and battle-tested
Key Takeaways:
- Use sessions for web apps, JWT for APIs
- Secure session configuration is critical
- Support multiple auth strategies easily
- Rate limit login attempts
- Always use HTTPS in production
Next Steps:
- Secure with bcrypt
- Add JWT
- Build Express API
Resources: