Implementing secure authentication is crucial for any production Node.js application. This guide covers JWT, sessions, OAuth, and security best practices.
1. Authentication Strategies
Session-Based
- Server stores session data
- Cookie with session ID
- Good for traditional web apps
- Uses
express-session
Token-Based (JWT)
- Stateless authentication
- Self-contained tokens
- Ideal for APIs and SPAs
- Uses
jsonwebtoken
OAuth/Third-Party
- Login via Google/Facebook/etc.
- Uses
passport.js
- Great for user convenience
- Still needs your own session/JWT
2. JWT Authentication Implementation
Setup and Configuration
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
// Store in environment variables
const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key';
const JWT_EXPIRES = process.env.JWT_EXPIRES || '30d';
User Registration
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
// In your controller
exports.register = async (req, res, next) => {
try {
const { email, password } = req.body;
// 1. Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already in use' });
}
// 2. Create new user
const user = await User.create({ email, password });
// 3. Generate token
const token = jwt.sign({ id: user._id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES
});
res.status(201).json({ token });
} catch (err) {
next(err);
}
};
3. Login and Token Generation
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
// 1. Check if user exists
const user = await User.findOne({ email }).select('+password');
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 2. Verify password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// 3. Generate token
const token = jwt.sign({ id: user._id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES
});
// 4. Send response
res.json({
token,
user: {
id: user._id,
email: user.email
}
});
} catch (err) {
next(err);
}
};
4. Protecting Routes with Middleware
Auth Middleware
exports.protect = async (req, res, next) => {
try {
// 1. Get token from header
let token;
if (req.headers.authorization &&
req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({
error: 'Not authorized to access this route'
});
}
// 2. Verify token
const decoded = jwt.verify(token, JWT_SECRET);
// 3. Get user from DB
const user = await User.findById(decoded.id);
if (!user) {
return res.status(401).json({
error: 'User no longer exists'
});
}
// 4. Attach user to request
req.user = user;
next();
} catch (err) {
next(err);
}
};
Role-Based Access
exports.restrictTo = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'You do not have permission'
});
}
next();
};
};
// Usage in routes:
router.get('/admin',
authController.protect,
authController.restrictTo('admin'),
adminController.getDashboard
);
5. Session-Based Authentication
Setup with express-session
const session = require('express-session');
const MongoStore = require('connect-mongo');
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.DB_URI
}),
cookie: {
maxAge: 24 * 60 * 60 * 1000, // 1 day
secure: process.env.NODE_ENV === 'production',
httpOnly: true
}
}));
Login with Sessions
exports.login = async (req, res, next) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Store user in session
req.session.user = {
id: user._id,
email: user.email,
role: user.role
};
res.json({ message: 'Logged in successfully' });
} catch (err) {
next(err);
}
};
6. OAuth with Passport.js
Google OAuth Setup
const passport = require('passport');
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) => {
// 1. Check if user exists
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// 2. Create new user if not exists
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName
});
}
done(null, user);
}));
// Serialize/deserialize user
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
OAuth Routes
// Initiate Google auth
router.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
// Google callback
router.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
session: true
}),
(req, res) => {
// Successful auth
res.redirect('/dashboard');
}
);
// Protected route example
router.get('/dashboard',
(req, res, next) => {
if (!req.isAuthenticated()) {
return res.redirect('/login');
}
next();
},
dashboardController
);
7. Security Best Practices
- Password Hashing: Always use bcrypt or Argon2
- HTTPS: Essential for production
- Cookie Security:
httpOnly
,secure
, andsameSite
flags - Rate Limiting: Prevent brute force attacks
- CSRF Protection: Use
csurf
middleware - Token Expiry: Short-lived access tokens with refresh tokens
- Input Validation: Never trust user input
Next: Error Handling and Debugging →
Authentication Checklist
- ✅ Secure password storage (hashing + salt)
- ✅ Protection against brute force attacks
- ✅ Session/token expiration
- ✅ Secure transmission (HTTPS only)
- ✅ Role-based access control
- ✅ Logging and monitoring