API Security Best Practices: Protecting Your Endpoints

Learn essential API security practices including authentication, authorization, input validation, rate limiting, and protecting against common vulnerabilities.

Why API Security Matters

APIs are the backbone of modern applications, but they're also prime targets for attackers. A single vulnerable endpoint can compromise your entire system. Implementing robust security measures is not optional—it's essential for protecting your data, users, and business reputation.

Common API Security Threats

  • Injection Attacks: SQL injection, NoSQL injection, command injection
  • Broken Authentication: Weak credentials, session management flaws
  • Sensitive Data Exposure: Unencrypted data transmission, weak encryption
  • XML External Entities (XXE): XML processing vulnerabilities
  • Broken Access Control: Inadequate authorization checks
  • Security Misconfiguration: Default settings, unnecessary features
  • Cross-Site Scripting (XSS): Client-side code injection
  • Insecure Deserialization: Object injection vulnerabilities
  • Insufficient Logging & Monitoring: Inadequate security monitoring

Authentication & Authorization

1. Implement Strong Authentication

Use industry-standard authentication methods:

// JWT-based authentication example
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Login endpoint
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Validate input
    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password required' });
    }
    
    // Find user and verify password
    const user = await User.findOne({ email });
    if (!user || !await bcrypt.compare(password, user.passwordHash)) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // Generate JWT token
    const token = jwt.sign(
      { userId: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    res.json({ token, user: { id: user.id, email: user.email } });
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

2. Implement Proper Authorization

// Role-based access control middleware
const authorize = (roles) => {
  return (req, res, next) => {
    const userRole = req.user.role;
    
    if (!roles.includes(userRole)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    
    next();
  };
};

// Usage
app.get('/api/admin/users', authenticateToken, authorize(['admin']), getUsers);
app.put('/api/users/:id', authenticateToken, authorize(['admin', 'user']), updateUser);

3. Use OAuth 2.0 and OpenID Connect

// OAuth 2.0 implementation with Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').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) => {
  // Handle user creation/authentication
  const user = await findOrCreateUser(profile);
  return done(null, user);
}));

// OAuth routes
app.get('/api/auth/google', passport.authenticate('google', {
  scope: ['profile', 'email']
}));

app.get('/api/auth/google/callback', 
  passport.authenticate('google', { session: false }),
  (req, res) => {
    // Generate JWT and redirect
    const token = jwt.sign({ userId: req.user.id }, process.env.JWT_SECRET);
    res.redirect(`/dashboard?token=${token}`);
  }
);

Input Validation & Sanitization

1. Validate All Input

// Using Joi for input validation
const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(120),
  password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required()
});

// Validation middleware
const validateUser = (req, res, next) => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ 
      error: 'Validation failed', 
      details: error.details[0].message 
    });
  }
  next();
};

app.post('/api/users', validateUser, createUser);

2. Sanitize Input Data

// Using express-validator for sanitization
const { body, validationResult } = require('express-validator');

const sanitizeInput = [
  body('name').trim().escape(),
  body('email').normalizeEmail(),
  body('description').trim().escape(),
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  }
];

3. Prevent SQL Injection

// Using parameterized queries (Prisma example)
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();

// ❌ Vulnerable to SQL injection
// const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = '${email}'`;

// ✅ Safe parameterized query
const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

// Or using Prisma's type-safe queries
const user = await prisma.user.findUnique({
  where: { email: email }
});

Rate Limiting & Throttling

1. Implement Rate Limiting

// Using express-rate-limit
const rateLimit = require('express-rate-limit');

// General rate limiting
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP, please try again later.',
  standardHeaders: true,
  legacyHeaders: false,
});

// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5, // limit each IP to 5 requests per windowMs
  message: 'Too many authentication attempts, please try again later.',
  skipSuccessfulRequests: true,
});

app.use('/api/', generalLimiter);
app.use('/api/auth/', authLimiter);

2. Advanced Rate Limiting with Redis

// Using express-rate-limit with Redis store
const RedisStore = require('rate-limit-redis');
const redis = require('redis');

const client = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  password: process.env.REDIS_PASSWORD
});

const redisLimiter = rateLimit({
  store: new RedisStore({
    client: client,
    prefix: 'rl:',
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: 'Too many requests, please try again later.'
});

Data Protection & Encryption

1. Use HTTPS Everywhere

// Force HTTPS in production
const helmet = require('helmet');

app.use(helmet({
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Redirect HTTP to HTTPS
if (process.env.NODE_ENV === 'production') {
  app.use((req, res, next) => {
    if (req.header('x-forwarded-proto') !== 'https') {
      res.redirect(`https://${req.header('host')}${req.url}`);
    } else {
      next();
    }
  });
}

2. Encrypt Sensitive Data

// Encrypting sensitive data at rest
const crypto = require('crypto');

class DataEncryption {
  constructor(secretKey) {
    this.algorithm = 'aes-256-gcm';
    this.secretKey = crypto.scryptSync(secretKey, 'salt', 32);
  }
  
  encrypt(text) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipher(this.algorithm, this.secretKey);
    cipher.setAAD(Buffer.from('additional data'));
    
    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    
    const authTag = cipher.getAuthTag();
    
    return {
      encrypted,
      iv: iv.toString('hex'),
      authTag: authTag.toString('hex')
    };
  }
  
  decrypt(encryptedData) {
    const decipher = crypto.createDecipher(
      this.algorithm, 
      this.secretKey
    );
    
    decipher.setAAD(Buffer.from('additional data'));
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
    
    let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    
    return decrypted;
  }
}

API Security Headers

Essential Security Headers

// Using helmet for security headers
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  crossOriginEmbedderPolicy: false,
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true
  }
}));

// Custom security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

Error Handling & Logging

1. Secure Error Handling

// Centralized error handling
const errorHandler = (err, req, res, next) => {
  // Log error details
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    timestamp: new Date().toISOString()
  });
  
  // Don't expose internal errors to client
  if (err.status) {
    res.status(err.status).json({
      error: err.message,
      timestamp: new Date().toISOString()
    });
  } else {
    res.status(500).json({
      error: 'Internal server error',
      timestamp: new Date().toISOString()
    });
  }
};

app.use(errorHandler);

2. Security Monitoring & Logging

// Security event logging
const winston = require('winston');

const securityLogger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.File({ filename: 'security.log' }),
    new winston.transports.Console()
  ]
});

// Log security events
const logSecurityEvent = (event, req, details = {}) => {
  securityLogger.info({
    event,
    ip: req.ip,
    userAgent: req.get('User-Agent'),
    url: req.url,
    method: req.method,
    userId: req.user?.id,
    timestamp: new Date().toISOString(),
    ...details
  });
};

// Usage
app.post('/api/auth/login', (req, res, next) => {
  // ... authentication logic
  if (loginSuccessful) {
    logSecurityEvent('LOGIN_SUCCESS', req, { userId: user.id });
  } else {
    logSecurityEvent('LOGIN_FAILED', req, { reason: 'Invalid credentials' });
  }
});

API Versioning & Deprecation

Version Your APIs

// API versioning strategies
// URL versioning
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);

// Header versioning
app.use((req, res, next) => {
  const apiVersion = req.headers['api-version'] || 'v1';
  req.apiVersion = apiVersion;
  next();
});

// Deprecation headers
app.use('/api/v1', (req, res, next) => {
  res.setHeader('Deprecation', 'true');
  res.setHeader('Sunset', '2025-12-31T23:59:59Z');
  res.setHeader('Link', '; rel="successor-version"');
  next();
});

Testing & Security Auditing

1. Automated Security Testing

// Using OWASP ZAP for security testing
const zap = require('zaproxy');

// Security test configuration
const securityTests = {
  sqlInjection: true,
  xss: true,
  csrf: true,
  authentication: true,
  authorization: true
};

// Run security tests
async function runSecurityTests() {
  const zap = new zap('localhost', 8080);
  await zap.spider.scan('http://localhost:3000');
  await zap.activeScan.scan('http://localhost:3000');
  const alerts = await zap.core.alerts();
  return alerts;
}

2. Dependency Scanning

// Using npm audit for dependency vulnerabilities
// package.json scripts
{
  "scripts": {
    "audit": "npm audit",
    "audit:fix": "npm audit fix",
    "security:check": "npm audit --audit-level moderate"
  }
}

// Using Snyk for advanced vulnerability scanning
// npx snyk test
// npx snyk monitor

Security Checklist

  • ✅ Implement strong authentication (JWT, OAuth 2.0)
  • ✅ Use proper authorization and RBAC
  • ✅ Validate and sanitize all input data
  • ✅ Implement rate limiting and throttling
  • ✅ Use HTTPS everywhere
  • ✅ Encrypt sensitive data at rest and in transit
  • ✅ Set proper security headers
  • ✅ Implement comprehensive logging and monitoring
  • ✅ Use parameterized queries to prevent SQL injection
  • ✅ Implement proper error handling
  • ✅ Regular security testing and auditing
  • ✅ Keep dependencies updated
  • ✅ Implement API versioning
  • ✅ Use CORS properly
  • ✅ Implement request/response validation

Test Your API Security

Use our OIDC playground and JWT verification tools to test and validate your API authentication and security implementations.

Use OIDC Playground Use JWT Verifier