Authentication & Security
Implement JWT, sessions, passport.js, and security best practices
Authentication in Express
Authentication verifies who a user is, while authorization determines what they can do. Express supports multiple authentication strategies including JWT, sessions, and OAuth.
🔐 Auth Strategies
JWT (Stateless)
Best for APIs and SPAs
Sessions (Stateful)
Best for traditional web apps
OAuth 2.0
Third-party login (Google, GitHub)
API Keys
Simple service-to-service auth
JWT Authentication
npm install jsonwebtoken bcrypt
npm install -D @types/jsonwebtoken @types/bcrypt
// utils/jwt.ts
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = '7d';
interface TokenPayload {
userId: string;
email: string;
role: string;
}
export const generateToken = (payload: TokenPayload): string => {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
};
export const verifyToken = (token: string): TokenPayload => {
return jwt.verify(token, JWT_SECRET) as TokenPayload;
};
export const generateRefreshToken = (userId: string): string => {
return jwt.sign({ userId }, JWT_SECRET, { expiresIn: '30d' });
};
Auth Routes
// routes/auth.ts
import { Router } from 'express';
import bcrypt from 'bcrypt';
import { generateToken, generateRefreshToken } from '../utils/jwt';
import { User } from '../models/User';
import { BadRequestError, UnauthorizedError } from '../errors/AppError';
const router = Router();
// Register
router.post('/register', async (req, res) => {
const { email, password, name } = req.body;
// Check if user exists
const existing = await User.findOne({ email });
if (existing) {
throw new BadRequestError('Email already registered');
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const user = await User.create({
email,
password: hashedPassword,
name
});
// Generate tokens
const token = generateToken({
userId: user.id,
email: user.email,
role: user.role
});
res.status(201).json({
user: { id: user.id, email: user.email, name: user.name },
token
});
});
// Login
router.post('/login', async (req, res) => {
const { email, password } = req.body;
// Find user
const user = await User.findOne({ email }).select('+password');
if (!user) {
throw new UnauthorizedError('Invalid credentials');
}
// Check password
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new UnauthorizedError('Invalid credentials');
}
// Generate token
const token = generateToken({
userId: user.id,
email: user.email,
role: user.role
});
res.json({ token });
});
export default router;
Auth Middleware
// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { verifyToken } from '../utils/jwt';
import { UnauthorizedError, ForbiddenError } from '../errors/AppError';
export interface AuthRequest extends Request {
user?: {
userId: string;
email: string;
role: string;
};
}
// Verify JWT
export const authenticate = (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedError('No token provided');
}
const token = authHeader.split(' ')[1];
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
throw new UnauthorizedError('Invalid or expired token');
}
};
// Check roles
export const authorize = (...roles: string[]) => {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
throw new UnauthorizedError('Not authenticated');
}
if (!roles.includes(req.user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
next();
};
};
// Usage
app.get('/api/admin', authenticate, authorize('admin'), (req, res) => {
res.json({ message: 'Admin access granted' });
});
Session Authentication
npm install express-session connect-mongo
npm install -D @types/express-session
import session from 'express-session';
import MongoStore from 'connect-mongo';
app.use(session({
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI!
}),
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Prevents XSS
maxAge: 24 * 60 * 60 * 1000, // 1 day
sameSite: 'strict' // CSRF protection
}
}));
// Login route with session
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email }).select('+password');
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new UnauthorizedError('Invalid credentials');
}
// Store user in session
req.session.userId = user.id;
req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email } });
});
// Logout
router.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) throw err;
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
Passport.js
npm install passport passport-local passport-jwt
npm install -D @types/passport @types/passport-local @types/passport-jwt
// config/passport.ts
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { Strategy as JwtStrategy, ExtractJwt } from 'passport-jwt';
import { User } from '../models/User';
// Local strategy (email/password)
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return done(null, false, { message: 'Invalid credentials' });
}
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// JWT strategy
passport.use(new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET!
},
async (payload, done) => {
try {
const user = await User.findById(payload.userId);
if (!user) return done(null, false);
return done(null, user);
} catch (error) {
return done(error);
}
}
));
// Usage
app.use(passport.initialize());
router.post('/login', passport.authenticate('local', { session: false }), (req, res) => {
const token = generateToken(req.user);
res.json({ token });
});
router.get('/protected', passport.authenticate('jwt', { session: false }), (req, res) => {
res.json({ user: req.user });
});
Security Best Practices
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import mongoSanitize from 'express-mongo-sanitize';
// Security headers
app.use(helmet());
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
message: { error: 'Too many requests' }
});
app.use('/api', limiter);
// Stricter limit for auth routes
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts
message: { error: 'Too many login attempts' }
});
app.use('/api/auth/login', authLimiter);
// Prevent NoSQL injection
app.use(mongoSanitize());
// Password requirements
const passwordSchema = z.string()
.min(8)
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special char');
// CORS configuration
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
🛡️ Security Checklist
- ☐ Use HTTPS in production
- ☐ Hash passwords with bcrypt (cost 12+)
- ☐ Use httpOnly cookies for tokens
- ☐ Implement rate limiting
- ☐ Validate and sanitize all input
- ☐ Use helmet for security headers
- ☐ Set secure cookie options (secure, sameSite)
- ☐ Never log passwords or tokens