Authentication & Security

JWT, sessions, and security best practices

JWT Authentication

typescript
// Auth & Security
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';

const SECRET = process.env.JWT_SECRET!;

// Login
async function login(email: string, password: string) {
  const user = await db.findUserByEmail(email);
  if (!user) throw new Error('User not found');

  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) throw new Error('Invalid password');

  const token = jwt.sign(
    { userId: user.id, role: user.role },
    SECRET,
    { expiresIn: '24h' }
  );
  return { token, user: { id: user.id, name: user.name } };
}

// Verify middleware
function requireAuth(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });
  try {
    req.user = jwt.verify(token, SECRET);
    next();
  } catch {
    res.status(403).json({ error: 'Invalid token' });
  }
}

// Role-based access
function requireRole(...roles: string[]) {
  return (req: any, res: any, next: any) => {
    if (!roles.includes(req.user?.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

Security Best Practices

  • Use helmet middleware — sets security headers (CSP, HSTS, etc.)
  • Enable CORS properly — whitelist specific origins, not '*'
  • Rate limiting — prevent brute force (express-rate-limit)
  • Input validation — use zod or joi for request body validation
  • SQL injection — use parameterized queries, never string concatenation
  • XSS prevention — sanitize user input, use CSP headers
  • HTTPS only — redirect HTTP to HTTPS in production
  • Environment variables — never commit secrets to code

Error Handling

typescript
// Global Error Handler
// Custom error class
class AppError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}

// Async wrapper (avoids try/catch in every route)
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) =>
  Promise.resolve(fn(req, res, next)).catch(next);

// Usage
app.get('/api/users/:id', asyncHandler(async (req, res) => {
  const user = await db.findUser(req.params.id);
  if (!user) throw new AppError(404, 'User not found');
  res.json(user);
}));

// Global error handler (MUST be last middleware, 4 params)
app.use((err: AppError, req: Request, res: Response, next: NextFunction) => {
  const status = err.statusCode || 500;
  res.status(status).json({
    error: status === 500 ? 'Internal server error' : err.message,
  });
});

💬 Where should you store JWT tokens on the client?

HttpOnly cookies are safest — they can't be accessed by JavaScript (prevents XSS theft). Avoid localStorage (vulnerable to XSS). If using cookies, add SameSite=Strict and Secure flags. For SPAs, httpOnly cookies with CSRF tokens is the gold standard.