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 });
});

📖 Passport.js Documentation →

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