Authentication Methods
There are several ways to handle authentication in Node.js applications. The right choice depends on your use case: session-based for traditional web apps, JWT for APIs and SPAs, or OAuth for third-party authentication.
🔑 Authentication Methods Compared
| Method | Best For | Storage |
|---|---|---|
| Sessions | Traditional web apps | Server (Redis) |
| JWT | APIs, SPAs, Mobile | Client (stateless) |
| OAuth 2.0 | Third-party login | Provider + tokens |
Password Hashing with bcrypt
bcrypt is battle-tested and widely used. For new projects, consider Argon2
(winner of Password Hashing Competition) as an alternative: npm install argon2
const bcrypt = require('bcrypt');
// Hash a password
async function hashPassword(plainPassword) {
const saltRounds = 12; // Higher = more secure but slower
const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
return hashedPassword;
}
// Verify a password
async function verifyPassword(plainPassword, hashedPassword) {
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
return isMatch;
}
// Usage in registration
app.post('/register', async (req, res) => {
const { email, password } = req.body;
// Validate password strength
if (password.length < 8) {
return res.status(400).json({ error: 'Password too short' });
}
const hashedPassword = await hashPassword(password);
await User.create({
email,
password: hashedPassword
});
res.status(201).json({ message: 'User created' });
});
// Usage in login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await verifyPassword(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session or JWT here
});
JWT Authentication
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '1h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
// Generate tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user.id, email: user.email },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES_IN }
);
return { accessToken, refreshToken };
}
// Login endpoint
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
const isValid = user && await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user);
// Store refresh token in httpOnly cookie
res.cookie('refreshToken', tokens.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({ accessToken: tokens.accessToken });
});
// Auth middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Protected route
app.get('/api/profile', authenticate, async (req, res) => {
const user = await User.findById(req.user.userId);
res.json({ user });
});
// Refresh token endpoint
app.post('/api/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
try {
const decoded = jwt.verify(refreshToken, JWT_SECRET);
if (decoded.type !== 'refresh') {
throw new Error('Invalid token type');
}
const accessToken = jwt.sign(
{ userId: decoded.userId },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
res.json({ accessToken });
} catch (error) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
Session-Based Authentication
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
// Redis client for session storage
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();
// Session middleware
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
name: 'sid',
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// Login
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
const isValid = user && await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
req.session.userId = user.id;
req.session.email = user.email;
res.json({ message: 'Logged in' });
});
// Auth middleware
function requireAuth(req, res, next) {
if (!req.session.userId) {
return res.status(401).json({ error: 'Not authenticated' });
}
next();
}
// Logout
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sid');
res.json({ message: 'Logged out' });
});
});
OAuth 2.0 with Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Configure Google OAuth
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 User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value
});
}
done(null, user);
} catch (error) {
done(error, null);
}
}
));
// Serialize user for session
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
// Initialize passport
app.use(passport.initialize());
app.use(passport.session());
// Auth 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');
}
);
// Protected route
app.get('/dashboard', (req, res) => {
if (!req.isAuthenticated()) {
return res.redirect('/login');
}
res.render('dashboard', { user: req.user });
});
Role-Based Access Control (RBAC)
// Define roles and permissions
const ROLES = {
admin: ['read', 'write', 'delete', 'manage_users'],
editor: ['read', 'write'],
viewer: ['read']
};
// Check permission middleware
function requirePermission(permission) {
return (req, res, next) => {
const userRole = req.user.role;
const permissions = ROLES[userRole] || [];
if (!permissions.includes(permission)) {
return res.status(403).json({ error: 'Permission denied' });
}
next();
};
}
// Usage
app.delete('/api/posts/:id',
authenticate,
requirePermission('delete'),
async (req, res) => {
await Post.findByIdAndDelete(req.params.id);
res.json({ message: 'Deleted' });
}
);
// Resource-based authorization
async function requireOwnership(req, res, next) {
const resource = await Post.findById(req.params.id);
if (!resource) {
return res.status(404).json({ error: 'Not found' });
}
if (resource.authorId !== req.user.userId && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Not authorized' });
}
req.resource = resource;
next();
}
app.put('/api/posts/:id',
authenticate,
requireOwnership,
async (req, res) => {
// User owns this post or is admin
req.resource.title = req.body.title;
await req.resource.save();
res.json(req.resource);
}
);
💡 Best Practices
- • Use bcrypt or argon2 for password hashing (never MD5/SHA1)
- • Store JWT refresh tokens in httpOnly cookies
- • Implement token rotation and revocation
- • Use short expiry for access tokens (15min-1hr)
- • Always validate and sanitize auth inputs
- • Implement account lockout after failed attempts