Skip to main content
โšก Calmops

API Security Essentials: CORS, API Keys & Rate Limiting

Table of Contents

API Security Essentials: CORS, API Keys & Rate Limiting

Building secure APIs is critical in today’s interconnected landscape. According to recent security reports, over 80% of web traffic is API-based, and API-related breaches increased by 200% in 2024. Three fundamental mechanisms form the backbone of modern API security: CORS (Cross-Origin Resource Sharing) controls which applications can access your API, API keys authenticate requests, and rate limiting prevents abuse and resource exhaustion.

Why This Matters:

  • APIs are often the primary attack vector for data breaches
  • Misconfigured CORS policies can expose sensitive data
  • Missing rate limiting can lead to DDoS attacks and server costs
  • Weak authentication mechanisms enable unauthorized access

What You’ll Learn:

  • How CORS works at the browser level and common misconfigurations
  • Implementing secure API key authentication with hashing
  • Building production-ready rate limiting with Redis
  • Combining multiple security layers for defense-in-depth
  • Advanced patterns like OAuth2, JWT, and mTLS
  • Testing, monitoring, and troubleshooting security implementations

This comprehensive guide covers practical implementation with real-world code examples, explaining the “why” behind each approach and showing how to avoid common pitfalls.


Table of Contents

  1. Why API Security Matters
  2. CORS: Controlling Cross-Origin Access
  3. API Keys: Authenticating Requests
  4. Rate Limiting: Preventing Abuse
  5. Advanced Authentication Methods
  6. Security Headers and Best Practices
  7. Testing Your API Security
  8. Monitoring and Incident Response
  9. Common Security Pitfalls
  10. Putting It All Together
  11. Production Deployment Checklist
  12. Conclusion

Why API Security Matters

When you expose an API publicly, you’re opening a door to your application. Without proper security, this door can be exploited:

  • Unauthorized access: Anyone could read or modify your data
  • Resource exhaustion: Attackers could overwhelm your servers
  • Data theft: Sensitive information could be stolen
  • Malicious clients: Bad actors could impersonate legitimate users

CORS, API keys, and rate limiting address these threats at different layers. Understanding how they workโ€”and how to misconfigure themโ€”is essential for any API developer.


CORS: Controlling Cross-Origin Access

What Is CORS?

CORS is a browser security mechanism that controls which external websites can access your API. Without CORS, a malicious website couldn’t steal data from your APIโ€”the browser would block the request.

The Same-Origin Policy

Browsers enforce the same-origin policy: a web page can only make requests to the same origin (protocol, domain, and port).

Page origin:    https://app.example.com:443
API origin:     https://api.example.com:443
Result:         โŒ BLOCKED (different domain)

Page origin:    https://example.com:443
API origin:     https://example.com:443
Result:         โœ… ALLOWED (same origin)

Page origin:    https://example.com:443
API origin:     https://example.com:8080
Result:         โŒ BLOCKED (different port)

This policy prevents websites from accessing sensitive data without permission. But legitimate cross-origin requests are common:

  • Your frontend (frontend.example.com) needs to call your API (api.example.com)
  • Third-party integrations need to access your API
  • Mobile apps need to fetch data

CORS provides a safe way to allow these requests while blocking malicious ones.

How CORS Works: Preflight Requests

For non-simple requests (anything with custom headers, PUT/DELETE methods, or JSON payloads), the browser sends a preflight request before the actual request:

1. Browser sends OPTIONS request:
   OPTIONS /api/users HTTP/1.1
   Origin: https://app.example.com
   Access-Control-Request-Method: POST
   Access-Control-Request-Headers: Content-Type

2. Server responds with CORS headers:
   Access-Control-Allow-Origin: https://app.example.com
   Access-Control-Allow-Methods: GET, POST, PUT, DELETE
   Access-Control-Allow-Headers: Content-Type

3. Browser checks: Is the origin allowed? Are these methods/headers allowed?
   
4. If yes, browser sends the actual request:
   POST /api/users HTTP/1.1
   Origin: https://app.example.com
   Content-Type: application/json
   { "name": "John" }

Implementing CORS Correctly

// Express.js CORS implementation
const express = require('express');
const cors = require('cors');

const app = express();

// โŒ WRONG - Allow all origins (too permissive)
app.use(cors());

// โœ… CORRECT - Whitelist specific origins
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
  'https://mobile.example.com'
];

app.use(cors({
  origin: (origin, callback) => {
    if (allowedOrigins.includes(origin) || !origin) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,  // Allow cookies
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 3600  // Cache preflight for 1 hour
}));

// Handle preflight requests explicitly (optional)
app.options('/api/*', cors());

app.post('/api/users', (req, res) => {
  res.json({ message: 'User created' });
});

CORS Headers Explained

Header Purpose Example
Access-Control-Allow-Origin Which origins can access the API https://app.example.com or *
Access-Control-Allow-Methods Which HTTP methods are allowed GET, POST, PUT, DELETE
Access-Control-Allow-Headers Which request headers are allowed Content-Type, Authorization
Access-Control-Allow-Credentials Whether cookies/auth headers are allowed true or false
Access-Control-Max-Age How long to cache preflight results 3600 (seconds)

Common CORS Misconfigurations

// โŒ WRONG - Too permissive
app.use(cors({
  origin: '*',  // Allow ANY origin
  credentials: true  // Also allow credentials? No, this will fail
}));

// โŒ WRONG - Regex that's too broad
app.use(cors({
  origin: /.*example.com/  // Matches attacker.example.com.attacker.com
}));

// โŒ WRONG - No credentials check
app.use(cors({
  origin: '*',
  credentials: true  // Contradiction! Browsers will reject this
}));

// โœ… CORRECT - Explicit whitelist
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
app.use(cors({
  origin: allowedOrigins,
  credentials: true
}));

API Keys: Authenticating Requests

What Are API Keys?

API keys are credentials that identify your application. They’re different from passwordsโ€”they don’t prove you are who you claim, but rather that you’re authorized to use the API.

API keys serve three purposes:

  1. Identification: Which application is making the request?
  2. Authorization: Is this application allowed to use the API?
  3. Rate limiting: How many requests has this key made today?

API Keys vs Other Authentication Methods

Method Use Case Security
API Key Public APIs, third-party integrations Low (single secret)
OAuth2 User on behalf of user, delegated access High (token-based, scoped)
mTLS Service-to-service communication Very High (certificate-based)
JWT Stateless, user authentication Medium (depends on implementation)

API keys are simple but less secure than OAuth2. Use them for:

  • Third-party integrations
  • Public APIs where authentication is “nice to have”
  • Simple service-to-service communication

Use OAuth2 for sensitive operations requiring user consent.

Where to Place API Keys

โŒ Query Parameters (Insecure)

GET /api/users?api_key=sk_live_abc123

Problems:

  • Keys appear in server logs
  • Keys appear in browser history
  • Keys appear in URL bar
  • Easy for attackers to intercept

โœ… HTTP Headers (Secure)

GET /api/users HTTP/1.1
Authorization: Bearer sk_live_abc123
// OR
X-API-Key: sk_live_abc123

Benefits:

  • Not logged in URLs
  • Encrypted with HTTPS
  • Standard HTTP mechanism
  • Can be restricted by the browser

Implementing API Key Authentication

// Middleware to verify API key
function verifyApiKey(req, res, next) {
  // Get key from Authorization header
  const authHeader = req.headers.authorization;
  const apiKey = authHeader && authHeader.split(' ')[1];
  
  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }
  
  // Verify key against database
  ApiKey.findOne({ key: apiKey, revoked: false }, async (err, keyDoc) => {
    if (err || !keyDoc) {
      return res.status(401).json({ error: 'Invalid API key' });
    }
    
    // Check if key has expired
    if (keyDoc.expiresAt && keyDoc.expiresAt < new Date()) {
      return res.status(401).json({ error: 'API key expired' });
    }
    
    // Attach key info to request
    req.apiKey = keyDoc;
    req.clientId = keyDoc.clientId;
    
    next();
  });
}

// Apply middleware to protected routes
app.get('/api/users', verifyApiKey, (req, res) => {
  res.json({ users: [], clientId: req.clientId });
});

// Key generation endpoint
app.post('/api/keys/generate', authenticate, async (req, res) => {
  const apiKey = crypto.randomBytes(32).toString('hex');
  const keyPrefix = 'sk_live_';  // Public prefix
  
  // Store hashed key (never store plaintext!)
  const hashedKey = crypto
    .createHash('sha256')
    .update(apiKey)
    .digest('hex');
  
  await ApiKey.create({
    key: hashedKey,
    clientId: req.user.id,
    expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),  // 1 year
    createdAt: new Date()
  });
  
  // Return key only once (unhashed)
  res.json({
    message: 'API key created',
    apiKey: keyPrefix + apiKey,
    warning: 'Save this key securely. You won\'t see it again.'
  });
});

API Key Best Practices

1. Rotate Keys Regularly

// Implement key rotation schedule
app.post('/api/keys/rotate', authenticate, async (req, res) => {
  const oldKey = req.body.keyId;
  
  // Create new key
  const newApiKey = crypto.randomBytes(32).toString('hex');
  const newKeyHash = crypto.createHash('sha256').update(newApiKey).digest('hex');
  
  // Mark old key as deprecated but don't delete (allow grace period)
  await ApiKey.findByIdAndUpdate(oldKey, {
    deprecated: true,
    rotatedAt: new Date()
  });
  
  // Create new key
  const newKey = await ApiKey.create({
    key: newKeyHash,
    clientId: req.user.id,
    expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000)
  });
  
  res.json({
    message: 'Key rotated',
    newApiKey: 'sk_live_' + newApiKey,
    oldKeyDeprecationPeriod: '30 days'
  });
});

2. Never Store Keys in Source Code

// โŒ WRONG - Key in code
const API_KEY = 'sk_live_abc123xyz';

// โœ… RIGHT - Key in environment variable
const API_KEY = process.env.API_KEY;

3. Use Hashed Keys in Database

// โŒ WRONG - Store plaintext
await ApiKey.create({ key: userProvidedKey });

// โœ… RIGHT - Store hash
const hashedKey = crypto.createHash('sha256').update(userProvidedKey).digest('hex');
await ApiKey.create({ key: hashedKey });

4. Implement Key Scopes

// API key with limited permissions
const apiKey = {
  id: 'key_123',
  clientId: 'client_456',
  scopes: ['read:users', 'write:orders'],  // Limited permissions
  expiresAt: new Date('2025-12-31'),
  rateLimit: {
    requests: 1000,
    window: '1 hour'
  }
};

// Enforce scopes
app.delete('/api/users/:id', verifyApiKey, (req, res) => {
  if (!req.apiKey.scopes.includes('delete:users')) {
    return res.status(403).json({ error: 'Permission denied' });
  }
  // Delete user...
});

Rate Limiting: Preventing Abuse

Why Rate Limiting Matters

Without rate limiting, a single malicious actor can:

  • Overwhelm your servers with requests
  • Brute-force passwords (try many combinations)
  • Scrape your entire database
  • Prevent legitimate users from accessing the API

Rate limiting throttles requests based on client, preventing abuse while allowing legitimate use.

Rate Limiting Strategies

Fixed Window Counter

Time: 00:00 - 01:00
Requests allowed: 1000
Current count: 950
User can make 50 more requests
Window resets at 01:00

Pros: Simple, easy to understand Cons: Requests can burst at window boundaries

Sliding Window Log

Last 60 minutes of requests: [00:05, 00:12, 00:45, 00:58, ...]
Count: 45
User can make 955 more requests
Always looks back 60 minutes

Pros: Accurate, prevents bursting Cons: Memory intensive (must store all request timestamps)

Token Bucket (Recommended)

Bucket capacity: 1000 tokens
Current tokens: 750
Refill rate: 10 tokens/second
Each request costs 1 token

Pros: Allows controlled bursting, smooth rate limiting Cons: Slightly more complex to implement

Implementing Rate Limiting

const Redis = require('redis');
const redis = Redis.createClient();

// Token bucket rate limiter
async function rateLimitTokenBucket(req, res, next) {
  const clientId = req.apiKey?.clientId || req.ip;
  const key = `rate_limit:${clientId}`;
  const capacity = 1000;      // Max tokens
  const refillRate = 10;      // Tokens per second
  const refillInterval = 1000; // Milliseconds
  
  try {
    // Get current bucket state
    const bucket = await redis.hGetAll(key);
    const now = Date.now();
    
    let tokens = bucket.tokens ? parseFloat(bucket.tokens) : capacity;
    const lastRefillTime = bucket.lastRefillTime ? parseInt(bucket.lastRefillTime) : now;
    
    // Refill tokens based on time elapsed
    const timePassed = now - lastRefillTime;
    const tokensToAdd = (timePassed / refillInterval) * refillRate;
    tokens = Math.min(capacity, tokens + tokensToAdd);
    
    // Check if request can proceed
    if (tokens < 1) {
      res.status(429).set({
        'Retry-After': Math.ceil((1 - tokens) / refillRate),
        'X-RateLimit-Limit': capacity,
        'X-RateLimit-Remaining': 0,
        'X-RateLimit-Reset': new Date(now + ((1 - tokens) / refillRate * 1000)).toISOString()
      }).json({
        error: 'Too many requests',
        retryAfter: Math.ceil((1 - tokens) / refillRate)
      });
      return;
    }
    
    // Consume token
    tokens -= 1;
    
    // Update bucket in Redis
    await redis.hSet(key, {
      tokens: tokens.toString(),
      lastRefillTime: now.toString()
    });
    await redis.expire(key, 3600); // Expire key after 1 hour
    
    // Add rate limit headers
    res.set({
      'X-RateLimit-Limit': capacity,
      'X-RateLimit-Remaining': Math.floor(tokens),
      'X-RateLimit-Reset': new Date(now + ((capacity - tokens) / refillRate * 1000)).toISOString()
    });
    
    next();
    
  } catch (error) {
    console.error('Rate limit error:', error);
    // On error, allow request (fail open)
    next();
  }
}

// Apply rate limiting
app.use(verifyApiKey);
app.use(rateLimitTokenBucket);

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

Rate Limit Headers

The HTTP standard defines headers to communicate rate limit status:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 750
X-RateLimit-Reset: 2025-12-12T14:30:00Z

// When limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 2025-12-12T14:30:00Z

Clients can use these headers to adjust their behavior before hitting the limit.

Tiered Rate Limiting

// Different limits for different clients
const rateLimits = {
  free: { requests: 100, window: '1 hour' },
  pro: { requests: 10000, window: '1 hour' },
  enterprise: { requests: Infinity }  // Unlimited
};

async function getTierRateLimit(clientId) {
  const tier = await Client.findById(clientId).select('tier');
  return rateLimits[tier.tier] || rateLimits.free;
}

app.use(async (req, res, next) => {
  const limit = await getTierRateLimit(req.clientId);
  req.rateLimit = limit;
  next();
});

Advanced Authentication Methods

OAuth 2.0: Delegated Authorization

OAuth 2.0 is the industry standard for authorization, allowing users to grant third-party applications limited access to their resources without sharing passwords.

OAuth 2.0 Flow (Authorization Code)

1. User clicks "Login with Google" on your app
2. Redirected to authorization server (Google)
3. User grants permission
4. Authorization server redirects back with authorization code
5. Your app exchanges code for access token
6. App uses access token to access user's resources

Implementation Example

const express = require('express');
const axios = require('axios');
const app = express();

// OAuth configuration
const oauth = {
  clientId: process.env.OAUTH_CLIENT_ID,
  clientSecret: process.env.OAUTH_CLIENT_SECRET,
  redirectUri: 'https://yourapp.com/callback',
  authorizationUrl: 'https://oauth-provider.com/authorize',
  tokenUrl: 'https://oauth-provider.com/token',
  scopes: ['read:profile', 'write:data']
};

// Step 1: Redirect to authorization server
app.get('/login', (req, res) => {
  const state = crypto.randomBytes(16).toString('hex'); // CSRF protection
  req.session.oauthState = state;
  
  const authUrl = `${oauth.authorizationUrl}?` +
    `client_id=${oauth.clientId}&` +
    `redirect_uri=${encodeURIComponent(oauth.redirectUri)}&` +
    `response_type=code&` +
    `scope=${oauth.scopes.join(' ')}&` +
    `state=${state}`;
  
  res.redirect(authUrl);
});

// Step 2: Handle callback with authorization code
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Verify state (CSRF protection)
  if (state !== req.session.oauthState) {
    return res.status(400).json({ error: 'Invalid state parameter' });
  }
  
  try {
    // Exchange authorization code for access token
    const tokenResponse = await axios.post(oauth.tokenUrl, {
      grant_type: 'authorization_code',
      code,
      redirect_uri: oauth.redirectUri,
      client_id: oauth.clientId,
      client_secret: oauth.clientSecret
    });
    
    const { access_token, refresh_token, expires_in } = tokenResponse.data;
    
    // Store tokens securely
    req.session.accessToken = access_token;
    req.session.refreshToken = refresh_token;
    req.session.tokenExpiry = Date.now() + (expires_in * 1000);
    
    res.redirect('/dashboard');
  } catch (error) {
    console.error('OAuth error:', error.response?.data || error.message);
    res.status(500).json({ error: 'Authentication failed' });
  }
});

// Middleware to verify access token
async function verifyOAuthToken(req, res, next) {
  const token = req.session.accessToken;
  
  if (!token) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  
  // Check if token expired
  if (Date.now() >= req.session.tokenExpiry) {
    // Attempt to refresh token
    try {
      const refreshResponse = await axios.post(oauth.tokenUrl, {
        grant_type: 'refresh_token',
        refresh_token: req.session.refreshToken,
        client_id: oauth.clientId,
        client_secret: oauth.clientSecret
      });
      
      req.session.accessToken = refreshResponse.data.access_token;
      req.session.tokenExpiry = Date.now() + (refreshResponse.data.expires_in * 1000);
    } catch (error) {
      return res.status(401).json({ error: 'Token refresh failed' });
    }
  }
  
  // Verify token with authorization server (optional but recommended)
  try {
    const verifyResponse = await axios.get('https://oauth-provider.com/userinfo', {
      headers: { Authorization: `Bearer ${token}` }
    });
    
    req.user = verifyResponse.data;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Protected endpoint
app.get('/api/profile', verifyOAuthToken, (req, res) => {
  res.json({ user: req.user });
});

JWT (JSON Web Tokens): Stateless Authentication

JWT enables stateless authenticationโ€”no need to store session data on the server.

JWT Structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Part 1: Header (algorithm & token type)
Part 2: Payload (claims/data)
Part 3: Signature (verification)

Implementation

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Secret key (store in environment variable)
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRY = '1h';
const REFRESH_TOKEN_EXPIRY = '7d';

// Login endpoint
app.post('/api/login', async (req, res) => {
  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 validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Generate access token
  const accessToken = jwt.sign(
    {
      sub: user.id,
      email: user.email,
      role: user.role
    },
    JWT_SECRET,
    {
      expiresIn: JWT_EXPIRY,
      issuer: 'yourapp.com',
      audience: 'api.yourapp.com'
    }
  );
  
  // Generate refresh token
  const refreshToken = jwt.sign(
    { sub: user.id, type: 'refresh' },
    JWT_SECRET,
    { expiresIn: REFRESH_TOKEN_EXPIRY }
  );
  
  // Store refresh token in database
  await RefreshToken.create({
    userId: user.id,
    token: refreshToken,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
  });
  
  res.json({
    accessToken,
    refreshToken,
    expiresIn: 3600
  });
});

// Middleware to verify JWT
function verifyJWT(req, res, next) {
  const authHeader = req.headers.authorization;
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'Token required' });
  }
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      issuer: 'yourapp.com',
      audience: 'api.yourapp.com'
    });
    
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Refresh token endpoint
app.post('/api/refresh', async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'Refresh token required' });
  }
  
  try {
    // Verify refresh token
    const decoded = jwt.verify(refreshToken, JWT_SECRET);
    
    // Check if token exists in database
    const storedToken = await RefreshToken.findOne({
      userId: decoded.sub,
      token: refreshToken
    });
    
    if (!storedToken) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    // Generate new access token
    const user = await User.findById(decoded.sub);
    const newAccessToken = jwt.sign(
      {
        sub: user.id,
        email: user.email,
        role: user.role
      },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRY }
    );
    
    res.json({ accessToken: newAccessToken });
  } catch (error) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// Protected route
app.get('/api/protected', verifyJWT, (req, res) => {
  res.json({ message: 'Protected data', user: req.user });
});

// Logout (invalidate refresh token)
app.post('/api/logout', verifyJWT, async (req, res) => {
  await RefreshToken.deleteMany({ userId: req.user.sub });
  res.json({ message: 'Logged out successfully' });
});

JWT Best Practices

// โœ… DO: Use strong secret keys
const JWT_SECRET = crypto.randomBytes(64).toString('hex');

// โœ… DO: Set appropriate expiration times
const accessTokenExpiry = '15m';   // Short-lived
const refreshTokenExpiry = '7d';   // Long-lived

// โœ… DO: Include necessary claims
const payload = {
  sub: user.id,        // Subject (user ID)
  iat: Date.now(),     // Issued at
  exp: expiry,         // Expiration
  iss: 'yourapp.com',  // Issuer
  aud: 'api.yourapp.com' // Audience
};

// โŒ DON'T: Store sensitive data in JWT
const badPayload = {
  password: 'secret',     // Never!
  creditCard: '1234...',  // Never!
  ssn: '123-45-6789'      // Never!
};

// โœ… DO: Validate all claims
jwt.verify(token, secret, {
  algorithms: ['HS256'],  // Specify allowed algorithms
  issuer: 'yourapp.com',
  audience: 'api.yourapp.com'
});

mTLS: Mutual TLS Authentication

For service-to-service communication, mTLS provides strong authentication using certificates.

const https = require('https');
const fs = require('fs');

// Server configuration with mTLS
const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  ca: fs.readFileSync('ca-cert.pem'),
  requestCert: true,        // Request client certificate
  rejectUnauthorized: true  // Reject invalid certificates
};

https.createServer(options, (req, res) => {
  // Client certificate is verified by TLS layer
  const clientCert = req.connection.getPeerCertificate();
  
  if (req.client.authorized) {
    console.log('Client authenticated:', clientCert.subject.CN);
    res.writeHead(200);
    res.end('Success');
  } else {
    console.log('Client not authorized:', req.connection.authorizationError);
    res.writeHead(401);
    res.end('Unauthorized');
  }
}).listen(8443);

// Client making mTLS request
const clientOptions = {
  hostname: 'api.example.com',
  port: 8443,
  path: '/api/data',
  method: 'GET',
  key: fs.readFileSync('client-key.pem'),
  cert: fs.readFileSync('client-cert.pem'),
  ca: fs.readFileSync('ca-cert.pem')
};

const req = https.request(clientOptions, (res) => {
  res.on('data', (d) => {
    console.log(d.toString());
  });
});

req.end();

When to Use Which Method

Method Use Case Security Level Complexity
API Keys Public APIs, simple authentication Low Low
OAuth 2.0 Third-party integrations, user authorization High Medium
JWT Stateless auth, microservices Medium-High Medium
mTLS Service-to-service, highly sensitive Very High High
Session Cookies Traditional web apps Medium Low

Security Headers and Best Practices

Essential Security Headers

const helmet = require('helmet');

// Apply security headers
app.use(helmet());

// Or configure individually
app.use((req, res, next) => {
  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');
  
  // Prevent MIME type sniffing
  res.setHeader('X-Content-Type-Options', 'nosniff');
  
  // Enable XSS protection
  res.setHeader('X-XSS-Protection', '1; mode=block');
  
  // Strict Transport Security (HTTPS only)
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  
  // Content Security Policy
  res.setHeader('Content-Security-Policy', "default-src 'self'");
  
  // Referrer policy
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // Permissions policy
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
  
  next();
});

Input Validation and Sanitization

const { body, validationResult } = require('express-validator');

// Validate and sanitize inputs
app.post('/api/users',
  [
    body('email')
      .isEmail()
      .normalizeEmail()
      .withMessage('Invalid email'),
    body('username')
      .isLength({ min: 3, max: 20 })
      .trim()
      .escape()
      .withMessage('Username must be 3-20 characters'),
    body('age')
      .optional()
      .isInt({ min: 0, max: 120 })
      .withMessage('Age must be between 0 and 120')
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Process validated data
    res.json({ message: 'User created' });
  }
);

SQL Injection Prevention

// โŒ VULNERABLE to SQL injection
app.get('/api/users/:id', (req, res) => {
  const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
  db.query(query); // Don't do this!
});

// โœ… SAFE: Use parameterized queries
app.get('/api/users/:id', (req, res) => {
  const query = 'SELECT * FROM users WHERE id = ?';
  db.query(query, [req.params.id]);
});

// โœ… SAFE: Use ORM
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user);
});

NoSQL Injection Prevention

// โŒ VULNERABLE to NoSQL injection
app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  // Attacker could send: { "username": { "$ne": null }, "password": { "$ne": null } }
  User.findOne({ username, password });
});

// โœ… SAFE: Sanitize inputs
const mongoSanitize = require('express-mongo-sanitize');

app.use(mongoSanitize());

app.post('/api/login', (req, res) => {
  const { username, password } = req.body;
  User.findOne({ username: String(username) });
});

Testing Your API Security

Unit Testing Security Middleware

const request = require('supertest');
const app = require('./app');

describe('API Security Tests', () => {
  describe('CORS', () => {
    it('should reject requests from unauthorized origins', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Origin', 'https://malicious.com');
      
      expect(res.status).toBe(403);
    });
    
    it('should allow requests from authorized origins', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Origin', 'https://app.example.com');
      
      expect(res.status).not.toBe(403);
      expect(res.headers['access-control-allow-origin'])
        .toBe('https://app.example.com');
    });
  });
  
  describe('API Key Authentication', () => {
    it('should reject requests without API key', async () => {
      const res = await request(app)
        .get('/api/users');
      
      expect(res.status).toBe(401);
      expect(res.body.error).toBe('API key required');
    });
    
    it('should reject requests with invalid API key', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', 'Bearer invalid_key');
      
      expect(res.status).toBe(401);
    });
    
    it('should accept requests with valid API key', async () => {
      const validKey = 'sk_test_valid_key_123';
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${validKey}`);
      
      expect(res.status).toBe(200);
    });
  });
  
  describe('Rate Limiting', () => {
    it('should allow requests within rate limit', async () => {
      for (let i = 0; i < 10; i++) {
        const res = await request(app)
          .get('/api/users')
          .set('Authorization', 'Bearer test_key');
        
        expect(res.status).toBe(200);
      }
    });
    
    it('should block requests exceeding rate limit', async () => {
      // Exhaust rate limit
      for (let i = 0; i < 100; i++) {
        await request(app)
          .get('/api/users')
          .set('Authorization', 'Bearer test_key');
      }
      
      // Next request should be rate limited
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', 'Bearer test_key');
      
      expect(res.status).toBe(429);
      expect(res.body.error).toContain('Rate limit');
      expect(res.headers['retry-after']).toBeDefined();
    });
    
    it('should include rate limit headers', async () => {
      const res = await request(app)
        .get('/api/users')
        .set('Authorization', 'Bearer test_key');
      
      expect(res.headers['x-ratelimit-limit']).toBeDefined();
      expect(res.headers['x-ratelimit-remaining']).toBeDefined();
      expect(res.headers['x-ratelimit-reset']).toBeDefined();
    });
  });
});

Security Penetration Testing

# Install security testing tools
npm install --save-dev owasp-zap newman

# Test for common vulnerabilities
# SQL Injection
curl -X GET "https://api.example.com/users?id=1' OR '1'='1"

# XSS
curl -X POST "https://api.example.com/comments" \
  -d '{"text":"<script>alert(1)</script>"}'

# CSRF
curl -X DELETE "https://api.example.com/users/123" \
  --cookie "session=victim_session"

# Command Injection
curl -X GET "https://api.example.com/ping?host=example.com;ls"

Load Testing Rate Limits

// Using Artillery for load testing
// artillery.yml
config:
  target: 'https://api.example.com'
  phases:
    - duration: 60
      arrivalRate: 100  # 100 requests/second
scenarios:
  - name: "Test rate limiting"
    flow:
      - get:
          url: "/api/users"
          headers:
            Authorization: "Bearer test_key"
# Run load test
artillery run artillery.yml

Monitoring and Incident Response

Real-Time Monitoring

const winston = require('winston');
const prometheus = require('prom-client');

// Set up logging
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'security.log' })
  ]
});

// Prometheus metrics
const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status']
});

const authFailures = new prometheus.Counter({
  name: 'auth_failures_total',
  help: 'Total number of authentication failures',
  labelNames: ['reason']
});

const rateLimitHits = new prometheus.Counter({
  name: 'rate_limit_hits_total',
  help: 'Total number of rate limit hits',
  labelNames: ['client_id']
});

// Monitoring middleware
app.use((req, res, next) => {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    
    httpRequestDuration
      .labels(req.method, req.route?.path || req.path, res.statusCode)
      .observe(duration);
    
    // Log security events
    if (res.statusCode === 401) {
      authFailures.inc({ reason: 'invalid_credentials' });
      logger.warn('Authentication failure', {
        ip: req.ip,
        path: req.path,
        userAgent: req.get('user-agent')
      });
    }
    
    if (res.statusCode === 429) {
      rateLimitHits.inc({ client_id: req.clientId });
      logger.warn('Rate limit exceeded', {
        clientId: req.clientId,
        ip: req.ip
      });
    }
  });
  
  next();
});

// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', prometheus.register.contentType);
  res.end(await prometheus.register.metrics());
});

Alerting System

const nodemailer = require('nodemailer');
const slack = require('slack-notify')(process.env.SLACK_WEBHOOK);

// Alert thresholds
const THRESHOLDS = {
  authFailures: 100,      // per minute
  rateLimitHits: 1000,    // per minute
  errorRate: 0.05         // 5%
};

// Check thresholds periodically
setInterval(async () => {
  const metrics = await getSecurityMetrics();
  
  if (metrics.authFailures > THRESHOLDS.authFailures) {
    sendAlert({
      severity: 'HIGH',
      title: 'High Authentication Failure Rate',
      message: `${metrics.authFailures} auth failures in last minute`,
      details: metrics.topFailingIPs
    });
  }
  
  if (metrics.rateLimitHits > THRESHOLDS.rateLimitHits) {
    sendAlert({
      severity: 'MEDIUM',
      title: 'High Rate Limit Hit Rate',
      message: `${metrics.rateLimitHits} rate limit hits in last minute`
    });
  }
}, 60000); // Check every minute

function sendAlert(alert) {
  // Log alert
  logger.error('Security alert', alert);
  
  // Send to Slack
  slack.alert({
    text: `๐Ÿšจ ${alert.title}`,
    fields: {
      Severity: alert.severity,
      Message: alert.message,
      Time: new Date().toISOString()
    }
  });
  
  // Send email for high severity
  if (alert.severity === 'HIGH') {
    sendEmailAlert(alert);
  }
}

Incident Response Playbook

1. API Key Compromise Detection

// Detect suspicious API key usage
app.use(async (req, res, next) => {
  if (!req.apiKey) return next();
  
  const usage = await getKeyUsagePattern(req.apiKey.id);
  
  // Anomaly detection
  if (usage.requestsPerMinute > usage.historicalAverage * 5) {
    logger.warn('Anomalous API key usage detected', {
      keyId: req.apiKey.id,
      current: usage.requestsPerMinute,
      average: usage.historicalAverage
    });
    
    // Auto-revoke key if very suspicious
    if (usage.requestsPerMinute > usage.historicalAverage * 10) {
      await revokeApiKey(req.apiKey.id);
      logger.error('API key auto-revoked', { keyId: req.apiKey.id });
      return res.status(401).json({ error: 'API key revoked' });
    }
  }
  
  next();
});

2. DDoS Mitigation

// Emergency rate limiting
function emergencyRateLimit(req, res, next) {
  const ip = req.ip;
  const key = `emergency:${ip}`;
  
  redis.incr(key, (err, count) => {
    if (err) return next();
    
    redis.expire(key, 60); // 1 minute window
    
    if (count > 10) {  // Very restrictive
      return res.status(429).json({ error: 'Service temporarily restricted' });
    }
    
    next();
  });
}

// Activate during attack
if (process.env.UNDER_ATTACK === 'true') {
  app.use(emergencyRateLimit);
}

Common Security Pitfalls

Pitfall 1: CORS Origin: * with Credentials

// โŒ WRONG - Browsers reject this combination
app.use(cors({
  origin: '*',
  credentials: true
}));

// โœ… CORRECT - Explicit origins OR no credentials
app.use(cors({
  origin: ['https://app.example.com'],
  credentials: true
}));

Pitfall 2: Storing API Keys in Plaintext

// โŒ WRONG
await ApiKey.create({ key: apiKey }); // Store plaintext

// โœ… CORRECT
const hash = crypto.createHash('sha256').update(apiKey).digest('hex');
await ApiKey.create({ key: hash }); // Store hash

Pitfall 3: Ignoring Rate Limit Circumvention

// โŒ WRONG - Rate limit by user ID only
app.use(rateLimit({ keyGenerator: (req) => req.user.id }));

// โœ… CORRECT - Rate limit by IP and user ID
app.use(rateLimit({
  keyGenerator: (req) => `${req.ip}:${req.user.id}`
}));

Pitfall 4: Sending API Keys in Responses

// โŒ WRONG - Logging keys
console.log('User API key:', userKey);

// โœ… CORRECT - Never log sensitive data
console.log('API key generated for client:', clientId);

Putting It All Together

Here’s a complete example combining all three mechanisms:

const express = require('express');
const cors = require('cors');
const redis = require('redis');

const app = express();
const redisClient = redis.createClient();

// 1. CORS - Allow specific origins
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// 2. API Key Authentication
async function verifyApiKey(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'Unauthorized' });
  
  const keyHash = crypto.createHash('sha256').update(token).digest('hex');
  const apiKey = await ApiKey.findOne({ key: keyHash });
  
  if (!apiKey) return res.status(401).json({ error: 'Invalid key' });
  req.clientId = apiKey.clientId;
  next();
}

// 3. Rate Limiting
async function rateLimit(req, res, next) {
  const limit = await getTierRateLimit(req.clientId);
  const bucket = await getRateLimitBucket(req.clientId);
  
  if (bucket.tokens < 1) {
    return res.status(429).json({
      error: 'Rate limit exceeded',
      retryAfter: 60
    });
  }
  
  await consumeToken(req.clientId);
  res.set('X-RateLimit-Remaining', Math.floor(bucket.tokens - 1));
  next();
}

app.use(verifyApiKey);
app.use(rateLimit);

// Protected endpoint
app.get('/api/data', (req, res) => {
  res.json({ data: 'sensitive information' });
});

app.listen(3000);

Production Deployment Checklist

Pre-Deployment Security Audit

Configuration Review

  • CORS origins explicitly whitelisted (no *)
  • API keys stored hashed in database
  • Rate limiting configured per tier
  • HTTPS enabled (no HTTP endpoints)
  • Environment variables used for secrets
  • Security headers configured (helmet.js)
  • Input validation on all endpoints
  • SQL/NoSQL injection prevention implemented
  • Authentication required on protected routes
  • Authorization checks on all mutations

Infrastructure Security

  • Firewall rules configured
  • Database access restricted to API server
  • Redis secured with authentication
  • Load balancer configured
  • DDoS protection enabled (Cloudflare/AWS Shield)
  • Backup strategy implemented
  • Monitoring and alerting set up
  • Logging configured (no sensitive data logged)
  • Certificate management automated (Let’s Encrypt)
  • Security patches applied to all dependencies

Testing Completed

  • Unit tests pass (>80% coverage)
  • Integration tests pass
  • Security penetration testing completed
  • Load testing performed
  • Rate limiting tested under load
  • CORS tested from multiple origins
  • API key rotation tested
  • Failure scenarios tested

Deployment Best Practices

// Environment-specific configuration
const config = {
  development: {
    corsOrigins: ['http://localhost:3000'],
    rateLimit: { requests: 1000000, window: '1h' },
    logLevel: 'debug'
  },
  staging: {
    corsOrigins: ['https://staging.example.com'],
    rateLimit: { requests: 10000, window: '1h' },
    logLevel: 'info'
  },
  production: {
    corsOrigins: [
      'https://app.example.com',
      'https://www.example.com'
    ],
    rateLimit: { requests: 1000, window: '1h' },
    logLevel: 'warn',
    requireHttps: true,
    strictCors: true
  }
};

const env = process.env.NODE_ENV || 'development';
module.exports = config[env];

Performance Optimization

// Cache API responses
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 });

app.get('/api/public/data', (req, res) => {
  const cacheKey = 'public_data';
  const cached = cache.get(cacheKey);
  
  if (cached) {
    res.set('X-Cache', 'HIT');
    return res.json(cached);
  }
  
  const data = fetchData();
  cache.set(cacheKey, data);
  res.set('X-Cache', 'MISS');
  res.json(data);
});

// Connection pooling for database
const { Pool } = require('pg');
const pool = new Pool({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  max: 20,              // Maximum connections
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000
});

// Compression
const compression = require('compression');
app.use(compression());

Graceful Shutdown

const server = app.listen(3000);

process.on('SIGTERM', () => {
  console.log('SIGTERM received, closing server gracefully...');
  
  server.close(() => {
    console.log('HTTP server closed');
    
    // Close database connections
    pool.end(() => {
      console.log('Database pool closed');
      
      // Close Redis connection
      redis.quit(() => {
        console.log('Redis connection closed');
        process.exit(0);
      });
    });
  });
  
  // Force shutdown after 30 seconds
  setTimeout(() => {
    console.error('Could not close connections in time, forcefully shutting down');
    process.exit(1);
  }, 30000);
});

Monitoring Dashboard Setup

# Grafana dashboard configuration
apiVersion: 1
providers:
  - name: 'Prometheus'
    type: prometheus
    url: http://prometheus:9090

dashboards:
  - title: "API Security Metrics"
    panels:
      - title: "Authentication Failures"
        type: graph
        targets:
          - expr: rate(auth_failures_total[5m])
      
      - title: "Rate Limit Hits"
        type: graph
        targets:
          - expr: rate(rate_limit_hits_total[5m])
      
      - title: "Response Times"
        type: graph
        targets:
          - expr: histogram_quantile(0.95, http_request_duration_seconds)
      
      - title: "Error Rate"
        type: graph
        targets:
          - expr: rate(http_requests_total{status=~"5.."}[5m])

Conclusion

API security is not optionalโ€”it’s foundational. CORS, API keys, and rate limiting form the essential first layer of defense:

The Three Pillars:

  1. CORS: Controls browser-based access, preventing unauthorized domains from accessing your API
  2. API Keys: Authenticates clients, enabling tracking and per-client rate limiting
  3. Rate Limiting: Protects infrastructure from abuse, DDoS attacks, and resource exhaustion

Beyond the Basics:

  • OAuth 2.0 for user authorization and third-party integrations
  • JWT for stateless, scalable authentication
  • mTLS for service-to-service communication
  • Security headers for additional browser protection
  • Monitoring for real-time threat detection

Key Takeaways:

โœ… Never use Access-Control-Allow-Origin: * with credentials โœ… Always hash API keys before storing โœ… Implement token bucket rate limiting for smooth throttling โœ… Use HTTPS everywhere (no exceptions) โœ… Validate and sanitize all inputs โœ… Monitor authentication failures and rate limit hits โœ… Have an incident response plan ready โœ… Test security under load โœ… Keep dependencies updated โœ… Follow the principle of least privilege

Real-World Impact:

  • APIs with proper rate limiting save $50,000-$500,000 annually in DDoS mitigation costs
  • OAuth 2.0 reduces password-related breaches by 90%
  • Properly configured CORS prevents 99% of cross-origin attacks
  • API key rotation reduces breach impact by 75%

Security Is a Journey:

API security is not a one-time implementationโ€”it requires continuous monitoring, testing, and improvement. Stay updated with:

  • OWASP API Security Top 10 updates
  • CVE databases for dependency vulnerabilities
  • Security advisories from framework maintainers
  • Industry best practices and emerging threats

Recommended Learning Path:

  1. Week 1-2: Implement CORS, API keys, and basic rate limiting
  2. Week 3-4: Add OAuth 2.0 and JWT authentication
  3. Week 5-6: Implement security headers and input validation
  4. Week 7-8: Set up monitoring, alerting, and logging
  5. Week 9-10: Perform security testing and penetration testing
  6. Week 11-12: Deploy to production with full security audit

Career Opportunities:

  • API Security Engineer: $90k-$180k
  • Security Architect: $130k-$250k
  • DevSecOps Engineer: $100k-$200k
  • Application Security Specialist: $95k-$190k

Your Next Steps:

  1. Audit your current API security implementation
  2. Implement missing security layers from this guide
  3. Set up monitoring and alerting
  4. Perform security testing
  5. Create incident response procedures
  6. Train your team on security best practices

Remember: Security is everyone’s responsibility. A chain is only as strong as its weakest link. By implementing defense-in-depth with CORS, API keys, rate limiting, and advanced authentication methods, you create a robust security posture that protects your users, your data, and your business.

Start securing your APIs todayโ€”the cost of prevention is always less than the cost of a breach.


Further Reading & Resources

Comments