Authentication & Authorization in Node.js
Authentication and Authorization are the cornerstones of application security. Authentication verifies who a user is, while authorization determines what they can do. In Node.js applications, implementing these correctly is critical — a single vulnerability can expose your entire system.
1. Authentication vs Authorization — The Difference
| Aspect | Authentication | Authorization |
|---|---|---|
| Question | "Who are you?" | "What can you do?" |
| Purpose | Verify user identity | Grant or deny access |
| When | During login | After authentication |
| Methods | Password, JWT, OAuth, Biometrics | Roles, Permissions, Policies |
| Analogy | Showing your ID at the door | Getting access to different rooms |
// Authentication checks who you are
// Authorization checks what you can do
// ❌ Without authentication — anyone can access
app.get('/api/admin/users', (req, res) => {
// No auth check — UNSAFE!
});
// ✅ With authentication and authorization
app.get('/api/admin/users', authenticateUser, authorizeRole('admin'), (req, res) => {
// User is authenticated AND has admin role
});
2. Authentication Strategies
2.1 Session-Based Authentication (Traditional)
Uses cookies and server-side sessions. Simple and works well for traditional web applications.
Installation
npm install express-session
npm install connect-mongo # For MongoDB session store
npm install bcrypt # For password hashing
npm install cookie-parser
Session-Based Auth Implementation
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const bcrypt = require('bcrypt');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.json());
app.use(cookieParser());
// ✅ Session configuration
app.use(session({
secret: process.env.SESSION_SECRET || 'your-secret-key',
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
ttl: 14 * 24 * 60 * 60 // 14 days
}),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // ✅ Can't be accessed by JavaScript
secure: process.env.NODE_ENV === 'production', // ✅ HTTPS only
sameSite: 'lax', // ✅ CSRF protection
maxAge: 24 * 60 * 60 * 1000 // 24 hours
}
}));
// ✅ Register user
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create user
const user = new User({
email,
password: hashedPassword,
name,
role: 'user'
});
await user.save();
// Create session
req.session.userId = user._id;
req.session.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.status(201).json({
message: 'Registration successful',
user: req.session.user
});
} catch (err) {
console.error('Registration error:', err);
res.status(500).json({ error: 'Registration failed' });
}
});
// ✅ Login
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = 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.user = {
id: user._id,
email: user.email,
name: user.name,
role: user.role
};
res.json({
message: 'Login successful',
user: req.session.user
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Login failed' });
}
});
// ✅ Logout
app.post('/api/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out successfully' });
});
});
// ✅ Authentication middleware
const authenticateUser = (req, res, next) => {
if (!req.session || !req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
next();
};
// ✅ Protected route
app.get('/api/profile', authenticateUser, (req, res) => {
res.json({ user: req.session.user });
});
2.2 JWT (JSON Web Token) Authentication — Modern API Standard
Stateless authentication suitable for APIs, mobile apps, and microservices.
Installation
npm install jsonwebtoken
npm install bcrypt
npm install dotenv
JWT Implementation
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { v4: uuidv4 } = require('uuid');
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
const JWT_EXPIRY = process.env.JWT_EXPIRY || '7d';
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '30d';
// ✅ Generate JWT tokens
function generateTokens(user) {
const accessToken = jwt.sign(
{ userId: user._id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
);
const refreshToken = jwt.sign(
{ userId: user._id },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRY }
);
return { accessToken, refreshToken };
}
// ✅ Register
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password, name } = req.body;
// Validate input
if (!email || !password || !name) {
return res.status(400).json({ error: 'All fields are required' });
}
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({ error: 'Email already registered' });
}
// Hash password
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Create user
const user = new User({
email,
password: hashedPassword,
name,
role: 'user'
});
await user.save();
// Generate tokens
const { accessToken, refreshToken } = generateTokens(user);
// Save refresh token (optional)
user.refreshToken = refreshToken;
await user.save();
res.status(201).json({
message: 'Registration successful',
user: { id: user._id, email: user.email, name: user.name, role: user.role },
accessToken,
refreshToken
});
} catch (err) {
console.error('Registration error:', err);
res.status(500).json({ error: 'Registration failed' });
}
});
// ✅ Login — JWT
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Verify password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate tokens
const { accessToken, refreshToken } = generateTokens(user);
// Save refresh token
user.refreshToken = refreshToken;
await user.save();
res.json({
message: 'Login successful',
user: { id: user._id, email: user.email, name: user.name, role: user.role },
accessToken,
refreshToken
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Login failed' });
}
});
// ✅ Refresh token
app.post('/api/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(400).json({ error: 'Refresh token required' });
}
// Verify refresh token
const decoded = jwt.verify(refreshToken, JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user || user.refreshToken !== refreshToken) {
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Generate new tokens
const tokens = generateTokens(user);
// Update refresh token
user.refreshToken = tokens.refreshToken;
await user.save();
res.json(tokens);
} catch (err) {
console.error('Refresh error:', err);
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// ✅ Logout
app.post('/api/auth/logout', async (req, res) => {
try {
const token = req.headers.authorization?.split(' ')[1];
if (token) {
// Invalidate refresh token
const decoded = jwt.verify(token, JWT_SECRET);
const user = await User.findById(decoded.userId);
if (user) {
user.refreshToken = null;
await user.save();
}
}
res.json({ message: 'Logged out successfully' });
} catch (err) {
res.json({ message: 'Logged out successfully' });
}
});
// ✅ JWT Authentication Middleware
const authenticateJWT = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
};
// ✅ Protected route with JWT
app.get('/api/profile', authenticateJWT, async (req, res) => {
try {
const user = await User.findById(req.user.userId).select('-password -refreshToken');
res.json({ user });
} catch (err) {
res.status(500).json({ error: 'Failed to fetch profile' });
}
});
2.3 OAuth 2.0 / Social Login (Google, GitHub, Facebook)
Allows users to authenticate using their existing accounts from third-party providers.
Installation (Passport.js)
npm install passport
npm install passport-google-oauth20
npm install passport-github
npm install express-session
OAuth Implementation with Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const GitHubStrategy = require('passport-github').Strategy;
// ✅ Google OAuth Strategy
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/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.findOne({ email: profile.emails[0].value });
if (user) {
user.googleId = profile.id;
await user.save();
} else {
user = new User({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value,
role: 'user',
isVerified: true
});
await user.save();
}
}
return done(null, user);
} catch (err) {
return done(err, null);
}
}));
// ✅ Routes for Google OAuth
app.get('/api/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/api/auth/google/callback',
passport.authenticate('google', { session: true, failureRedirect: '/login' }),
(req, res) => {
// Generate JWT or start session
const token = jwt.sign(
{ userId: req.user._id, email: req.user.email, role: req.user.role },
JWT_SECRET,
{ expiresIn: '7d' }
);
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${token}`);
}
);
3. Authorization — Role-Based Access Control (RBAC)
RBAC restricts system access based on user roles and permissions.
Role & Permission Models
// models/Role.js
const mongoose = require('mongoose');
const roleSchema = new mongoose.Schema({
name: { type: String, required: true, unique: true },
permissions: [{ type: String }],
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('Role', roleSchema);
// models/User.js — Add role field
const userSchema = new mongoose.Schema({
// ... other fields
role: {
type: String,
enum: ['user', 'moderator', 'admin', 'superadmin'],
default: 'user'
},
permissions: [{ type: String }] // For granular permissions
});
Authorization Middleware
// middleware/authorization.js
// ✅ Check if user has a specific role
const authorizeRole = (...allowedRoles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// For JWT: req.user is from token
// For Session: req.user from session
const userRole = req.user.role || req.session?.user?.role;
if (!userRole || !allowedRoles.includes(userRole)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// ✅ Check if user has specific permissions (granular)
const authorizePermission = (requiredPermission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userPermissions = req.user.permissions || req.session?.user?.permissions || [];
if (!userPermissions.includes(requiredPermission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// ✅ Check if user owns a resource (resource-based authorization)
const authorizeResourceOwner = (getResourceId, getUserId) => {
return async (req, res, next) => {
try {
const resourceId = getResourceId(req);
const userId = getUserId(req);
// This example uses JWT, adapt for session
const resource = await Resource.findById(resourceId);
if (!resource) {
return res.status(404).json({ error: 'Resource not found' });
}
if (resource.userId.toString() !== userId.toString()) {
return res.status(403).json({ error: 'You can only access your own resources' });
}
next();
} catch (err) {
next(err);
}
};
};
// ✅ Combine authentication + role authorization
const authAndRole = (allowedRoles) => {
return [authenticateJWT, authorizeRole(...allowedRoles)];
};
module.exports = {
authorizeRole,
authorizePermission,
authorizeResourceOwner,
authAndRole
};
Protected Routes with Authorization
const { authenticateJWT } = require('../middleware/auth');
const { authorizeRole, authorizePermission } = require('../middleware/authorization');
// ✅ Only admins can access
app.get('/api/admin/users',
authenticateJWT,
authorizeRole('admin', 'superadmin'),
async (req, res) => {
const users = await User.find().select('-password -refreshToken');
res.json(users);
}
);
// ✅ Only superadmins can delete users
app.delete('/api/admin/users/:id',
authenticateJWT,
authorizeRole('superadmin'),
async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.status(204).send();
}
);
// ✅ Granular permission check
app.post('/api/posts',
authenticateJWT,
authorizePermission('create:posts'),
async (req, res) => {
// Create post
}
);
// ✅ Resource ownership check
app.put('/api/posts/:id',
authenticateJWT,
authorizeResourceOwner(
(req) => req.params.id,
(req) => req.user.userId
),
async (req, res) => {
// Update post (user owns it)
}
);
4. Security Best Practices — Production Checklist
Password Security
- ✅ Use bcrypt with salt rounds ≥ 12 —
bcrypt.hash(password, 12) - ✅ Never store plaintext passwords — Always hash before saving.
- ✅ Require strong passwords — Minimum 8 characters, mix of cases, numbers, symbols.
- ✅ Implement rate limiting — Prevent brute force attacks.
- ✅ Use account lockout — Lock account after multiple failed attempts.
JWT Security
- ✅ Use strong secrets — 256-bit+ secrets from environment variables.
- ✅ Set short expiration times — 15 minutes for access tokens, 7 days for refresh tokens.
- ✅ Store tokens securely — Use HTTP-only cookies or secure storage.
- ✅ Implement token rotation — Issue new refresh tokens on each refresh.
- ✅ Blacklist tokens — Maintain a token blacklist for logout/revocation.
Session Security
- ✅ Use secure cookies —
{ httpOnly: true, secure: true, sameSite: 'lax' } - ✅ Use a session store — Redis or MongoDB for persistent sessions.
- ✅ Set session expiration — Automatically expire inactive sessions.
- ✅ Regenerate session ID on login — Prevent session fixation attacks.
General Security
- ✅ Use Helmet.js —
app.use(helmet())for security headers. - ✅ Enable CORS properly — Only allow trusted origins.
- ✅ Use HTTPS in production — Always use TLS/SSL.
- ✅ Validate all input — Use express-validator or Joi.
- ✅ Sanitize output — Prevent XSS attacks.
- ✅ Implement rate limiting —
express-rate-limit. - ✅ Keep dependencies updated — Run
npm auditregularly. - ✅ Use environment variables — Never hardcode secrets.
5. Common Security Pitfalls & Solutions
Pitfall 1: Storing JWT in localStorage
Problem: Vulnerable to XSS attacks.
Solution: Use HTTP-only cookies instead.
// ✅ HTTP-only cookie for JWT
res.cookie('accessToken', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 15 * 60 * 1000 // 15 minutes
});
Pitfall 2: Not implementing rate limiting
Problem: Brute force attacks on login endpoints.
Solution: Use express-rate-limit.
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many login attempts. Please try again later.'
});
app.post('/api/auth/login', loginLimiter, handleLogin);
Pitfall 3: Returning too much information in errors
Problem: Exposes internal details to attackers.
Solution: Only return generic error messages.
// ❌ Bad — exposes internal info
res.status(401).json({ error: 'User with email john@example.com not found' });
// ✅ Good — generic message
res.status(401).json({ error: 'Invalid credentials' });
6. Authentication Comparison — Choosing the Right Method
| Factor | Session-Based | JWT | OAuth 2.0 |
|---|---|---|---|
| State | Stateful (server stores session) | Stateless | Stateful |
| Scalability | Requires shared session store | Highly scalable | Requires token management |
| Mobile Apps | Works with cookies | ✅ Best for mobile | ✅ Excellent |
| Microservices | ❌ Complex | ✅ Ideal | ✅ Good |
| Logout | ✅ Immediate | ❌ Requires blacklist | ✅ With revocation |
| Implementation | Simple | Moderate | Complex |
| Use Case | Traditional web apps | APIs, SPAs, mobile | Social login, SSO |
7. Production-Ready Checklist
- ✅ Use bcrypt — For password hashing with salt rounds ≥ 12.
- ✅ Implement JWT — With short-lived access tokens and refresh tokens.
- ✅ Use HTTP-only cookies — For JWT storage (instead of localStorage).
- ✅ Implement RBAC — Role-based access control.
- ✅ Add rate limiting — Protect login and registration endpoints.
- ✅ Use Helmet.js — Security headers.
- ✅ Validate input — Use express-validator.
- ✅ Sanitize output — Prevent XSS.
- ✅ Enable HTTPS — Always in production.
- ✅ Use environment variables — For all secrets.
- ✅ Implement token rotation — For refresh tokens.
- ✅ Add session expiration — Auto-logout inactive users.
- ✅ Log security events — Failed logins, permission denials.
- ✅ Run security audits —
npm auditregularly.
Ready to master Node.js?
Build scalable, high-performance backend applications with Node.js. Learn from industry experts with hands-on projects.
.png)