Skip to main content
🔐Security

API Security: Comprehensive Guide to Protecting Your APIs

APIs are the backbone of modern applications, connecting frontends, mobile apps, microservices, and third-party integrations. This makes them a prime attac...

📖 8 min read

API Security: Comprehensive Guide to Protecting Your APIs

APIs are the backbone of modern applications, connecting frontends, mobile apps, microservices, and third-party integrations. This makes them a prime attack target. A single vulnerable API endpoint can expose millions of user records. This guide covers every layer of API security, from authentication and authorization to input validation, rate limiting, and security headers.

API Authentication Methods

Method Security Level Best For Limitations
API Keys Low Identifying callers, public APIs Not user-specific, easily leaked
OAuth 2.0 Bearer Tokens High User-facing apps, delegated access Complex setup
JWT High Stateless APIs, microservices Hard to revoke
mTLS (Mutual TLS) Very High Service-to-service, zero trust Certificate management overhead
HMAC Signatures High Webhook verification, AWS-style APIs Shared secret distribution

API Key vs Bearer Token

API keys identify the calling application. Bearer tokens (from OAuth 2.0) identify and authorize the user. Many APIs use both: an API key for app identification and rate limiting, plus a Bearer token for user authorization.

// API key in header (identifies app)
X-API-Key: sk_live_abc123def456

// Bearer token (identifies and authorizes user)
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

Authentication Middleware

const jwt = require('jsonwebtoken');

// JWT authentication middleware
function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({
      error: 'authentication_required',
      message: 'Missing or invalid Authorization header'
    });
  }

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, publicKey, {
      algorithms: ['RS256'],
      issuer: 'https://auth.myapp.com'
    });
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({
      error: 'invalid_token',
      message: err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token'
    });
  }
}

// Authorization middleware
function authorize(...requiredPermissions) {
  return (req, res, next) => {
    const userPermissions = req.user.permissions || [];
    const hasPermission = requiredPermissions.every(
      perm => userPermissions.includes(perm)
    );
    if (!hasPermission) {
      return res.status(403).json({
        error: 'insufficient_permissions',
        required: requiredPermissions
      });
    }
    next();
  };
}

// Usage
app.get('/api/users', authenticate, authorize('users:read'), listUsers);
app.delete('/api/users/:id', authenticate, authorize('users:delete'), deleteUser);

Learn more about token management in our JWT guide and Authentication vs Authorization overview.

Rate Limiting

Rate limiting prevents API abuse by restricting the number of requests a client can make within a time window. It protects against brute force attacks, DDoS, and resource exhaustion.

const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL);

// General API rate limit
const apiLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                    // 100 requests per window
  standardHeaders: true,       // Return rate limit info in headers
  legacyHeaders: false,
  keyGenerator: (req) => req.user?.id || req.ip,
  message: {
    error: 'rate_limit_exceeded',
    retryAfter: 'Check Retry-After header'
  }
});

// Strict rate limit for sensitive endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts. Try again in 15 minutes.' }
});

app.use('/api/', apiLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/reset-password', authLimiter);

Input Validation

Never trust client input. Validate every field for type, length, format, and range. Use a schema validation library rather than manual checks.

const Joi = require('joi');

// Define validation schemas
const schemas = {
  createUser: Joi.object({
    name: Joi.string().min(2).max(100).trim().required(),
    email: Joi.string().email().lowercase().required(),
    age: Joi.number().integer().min(13).max(150),
    role: Joi.string().valid('user', 'editor').default('user'),
    password: Joi.string()
      .min(8).max(128)
      .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])/)
      .required()
      .messages({
        'string.pattern.base': 'Password must contain uppercase, lowercase, number, and special character'
      })
  }),

  getUsers: Joi.object({
    page: Joi.number().integer().min(1).default(1),
    limit: Joi.number().integer().min(1).max(100).default(20),
    sort: Joi.string().valid('name', 'createdAt', '-name', '-createdAt')
  })
};

// Validation middleware factory
function validate(schemaName, source = 'body') {
  return (req, res, next) => {
    const { error, value } = schemas[schemaName].validate(req[source], {
      abortEarly: false,
      stripUnknown: true  // Remove unexpected fields
    });
    if (error) {
      return res.status(400).json({
        error: 'validation_error',
        details: error.details.map(d => ({
          field: d.path.join('.'),
          message: d.message
        }))
      });
    }
    req[source] = value;  // Use sanitized values
    next();
  };
}

app.post('/api/users', authenticate, validate('createUser'), createUser);
app.get('/api/users', authenticate, validate('getUsers', 'query'), listUsers);

CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which domains can access your API. Misconfigured CORS is a common vulnerability.

const cors = require('cors');

// SECURE: Whitelist specific origins
const corsOptions = {
  origin: function (origin, callback) {
    const allowedOrigins = [
      'https://myapp.com',
      'https://admin.myapp.com',
      'https://staging.myapp.com'
    ];
    // Allow requests with no origin (mobile apps, curl)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,       // Allow cookies
  maxAge: 86400            // Cache preflight for 24 hours
};

app.use(cors(corsOptions));

// INSECURE — never do this in production:
// app.use(cors({ origin: '*', credentials: true }));

CSRF Protection

Cross-Site Request Forgery tricks authenticated users into making unwanted requests. APIs using cookies for authentication must implement CSRF protection.

const csrf = require('csurf');

// For cookie-based auth, use CSRF tokens
const csrfProtection = csrf({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  }
});

// Provide token to client
app.get('/api/csrf-token', csrfProtection, (req, res) => {
  res.json({ csrfToken: req.csrfToken() });
});

// Protect state-changing endpoints
app.post('/api/transfer', csrfProtection, authenticate, transferFunds);

// For Bearer token APIs, CSRF is not needed — tokens are not sent automatically

SQL and NoSQL Injection Prevention

// SQL INJECTION — VULNERABLE
const query = `SELECT * FROM users WHERE id = '${req.params.id}'`;

// SQL INJECTION — SAFE (parameterized query)
const query = 'SELECT * FROM users WHERE id = $1';
const result = await pool.query(query, [req.params.id]);

// NoSQL INJECTION — VULNERABLE
db.users.find({ username: req.body.username, password: req.body.password });
// Attacker sends: { "username": "admin", "password": { "$ne": "" } }

// NoSQL INJECTION — SAFE (type validation + sanitization)
const username = String(req.body.username);  // Force string type
const password = String(req.body.password);
db.users.find({ username, password: await bcrypt.hash(password) });

// Use an ORM with built-in protection
const user = await User.findOne({
  where: { id: req.params.id }  // Sequelize auto-parameterizes
});

Security Headers

const helmet = require('helmet');

app.use(helmet());  // Sets many secure headers at once

// Or configure individually:
app.use((req, res, next) => {
  // Prevent XSS
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-XSS-Protection', '0');  // Rely on CSP instead

  // Prevent clickjacking
  res.setHeader('X-Frame-Options', 'DENY');

  // Content Security Policy
  res.setHeader('Content-Security-Policy', "default-src 'self'");

  // Force HTTPS
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

  // Control referrer information
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

  // Permissions policy
  res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');

  next();
});

API Gateway Security

An API gateway centralizes security concerns, providing a single enforcement point for authentication, rate limiting, and request validation. This follows zero trust principles by ensuring every request is verified.

Security Layer API Gateway Application
TLS Termination Yes Optional (internal mTLS)
Authentication Token validation, API key check Fine-grained authorization
Rate Limiting Global and per-client limits Endpoint-specific limits
Request Validation Schema validation, size limits Business logic validation
IP Filtering Allowlist/blocklist Not typically

Logging and Monitoring

// Security-focused request logging
function securityLogger(req, res, next) {
  const start = Date.now();

  res.on('finish', () => {
    const logEntry = {
      timestamp: new Date().toISOString(),
      method: req.method,
      path: req.originalUrl,
      statusCode: res.statusCode,
      ip: req.ip,
      userAgent: req.headers['user-agent'],
      userId: req.user?.sub || 'anonymous',
      duration: Date.now() - start,
      // Flag suspicious activity
      suspicious: res.statusCode === 401 || res.statusCode === 403
    };

    if (logEntry.suspicious) {
      alertSecurityTeam(logEntry);
    }

    logger.info('api_request', logEntry);
  });

  next();
}

Use our API and Network Tools to test your API security configuration and our Security Crypto Tools to validate encryption settings.

API Security Checklist

  • Use HTTPS everywhere — redirect HTTP to HTTPS
  • Implement proper authentication (OAuth 2.0 or JWT)
  • Apply rate limiting on all endpoints, stricter on auth endpoints
  • Validate and sanitize all inputs with schema validation
  • Use parameterized queries to prevent injection attacks
  • Configure CORS correctly — never use wildcard with credentials
  • Set security headers (Helmet.js or equivalent)
  • Log all requests with security-relevant metadata
  • Implement request size limits to prevent payload attacks
  • Version your API and deprecate insecure older versions
  • Use an API gateway for centralized security enforcement
  • Monitor for anomalies and set up automated alerts

Frequently Asked Questions

Should I use API keys or OAuth tokens?

Use API keys for simple identification and rate limiting of server-to-server calls where you control both endpoints. Use OAuth 2.0 tokens when third parties need delegated user access, when you need scoped permissions, or for any user-facing application. Many production APIs use both: API keys for app identification and OAuth tokens for user authorization.

How do I protect against API scraping?

Combine multiple defenses: rate limiting per API key and per IP, requiring authentication for data-rich endpoints, implementing pagination limits, monitoring for unusual access patterns, using CAPTCHAs for suspicious behavior, and fingerprinting clients beyond just IP addresses. No single measure is sufficient — defense in depth is essential.

Is CSRF protection necessary for APIs?

If your API uses cookie-based authentication (session cookies), yes — CSRF protection is mandatory. If your API uses Bearer tokens in the Authorization header, CSRF protection is not needed because browsers do not automatically attach Authorization headers to cross-origin requests. Most modern SPAs use Bearer tokens and are inherently CSRF-safe.

How should I handle API versioning securely?

Use URL path versioning (e.g., /v1/, /v2/) or header-based versioning. When releasing new versions, deprecate old versions with warnings before removal. Never maintain insecure old API versions indefinitely — set a sunset date and enforce migration. Security patches should be applied to all active versions simultaneously.

What is the best way to secure internal microservice APIs?

Use mutual TLS (mTLS) for service-to-service authentication in zero trust architectures. Implement service mesh tools like Istio or Linkerd for automatic mTLS. Use JWTs for propagating user context between services. Apply network policies to restrict which services can communicate. Never expose internal APIs to the internet. Explore more at swehelper.com/tools.

Related Articles