Skip to content

API Security Best Practices

Alex Stojcic edited this page Apr 3, 2025 · 2 revisions

API Security Best Practices

APIs (Application Programming Interfaces) are the connective tissue of modern applications. Securing APIs is critical as they often expose sensitive data and functionality. This guide covers comprehensive API security best practices to protect your services.

API Security Fundamentals

Types of API Vulnerabilities

  1. Authentication flaws - Weak or missing authentication mechanisms
  2. Authorization issues - Improper access controls
  3. Input validation gaps - Accepting and processing malicious input
  4. Excessive data exposure - Returning more data than necessary
  5. Resource constraints lacking - Allowing excessive usage or abuse
  6. Security misconfigurations - Insecure default settings
  7. Injection flaws - Command or query injection vulnerabilities
  8. Improper assets management - Unpatched or deprecated API versions

Authentication & Authorization

API Authentication Methods

Method Description Best For
API Keys Simple secret tokens Low-risk internal APIs, developer access
Bearer Tokens Tokens passed in headers Most API scenarios, especially JWTs
OAuth 2.0 Token delegation framework Complex permissions, third-party integrations
mTLS Mutual certificate authentication High-security environments

Implementing JWT Authentication

// Node.js/Express example with JWT
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

// Middleware to authenticate JWT function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1];

if (!token) return res.status(401).json({ error: 'Access token required' });

jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid or expired token' }); req.user = user; next(); }); }

// Protected route app.get('/api/protected-resource', authenticateToken, (req, res) => { res.json({ data: 'This is protected data', user: req.user }); });

// Token issuance (login) app.post('/api/login', (req, res) => { // Verify credentials (simplified) const user = authenticateUser(req.body.username, req.body.password);

if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); }

// Create tokens const accessToken = jwt.sign( { id: user.id, username: user.username, role: user.role }, process.env.JWT_SECRET, { expiresIn: '15m' } );

const refreshToken = jwt.sign( { id: user.id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' } );

// Store refresh token securely (e.g., database) saveRefreshToken(user.id, refreshToken);

res.json({ accessToken, refreshToken, expiresIn: 900 // 15 minutes in seconds }); });

// Token refresh endpoint app.post('/api/refresh-token', (req, res) => { const refreshToken = req.body.refreshToken;

if (!refreshToken) { return res.status(401).json({ error: 'Refresh token required' }); }

// Verify token is valid and in our database if (!isValidRefreshToken(refreshToken)) { return res.status(403).json({ error: 'Invalid refresh token' }); }

jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => { if (err) return res.status(403).json({ error: 'Invalid token' });

// Get full user data for new access token
const userData = getUserData(user.id);

const accessToken = jwt.sign(
  { id: userData.id, username: userData.username, role: userData.role },
  process.env.JWT_SECRET,
  { expiresIn: '15m' }
);

res.json({ 
  accessToken, 
  expiresIn: 900 // 15 minutes in seconds
});

}); });

API Authorization with Role-Based Access Control

// Middleware for role-based authorization
function checkRole(roles) {
  return (req, res, next) => {
    // Get user from JWT authentication middleware
    const user = req.user;
if (!user) {
  return res.status(401).json({ error: 'Authentication required' });
}

if (!roles.includes(user.role)) {
  return res.status(403).json({ error: 'Insufficient permissions' });
}

next();

}; }

// Apply role checks to routes app.get('/api/users', authenticateToken, checkRole(['admin']), (req, res) => { // Only admins can access this endpoint res.json({ users: getAllUsers() }); } );

app.get('/api/products', authenticateToken, checkRole(['admin', 'manager', 'viewer']), (req, res) => { // Multiple roles can access this endpoint res.json({ products: getProducts() }); } );

Fine-Grained Permission Control

// Permission-based authorization (more flexible than roles)
function checkPermission(permission) {
  return async (req, res, next) => {
    const user = req.user;
if (!user) {
  return res.status(401).json({ error: 'Authentication required' });
}

// Get user permissions from database
const userPermissions = await getUserPermissions(user.id);

if (!userPermissions.includes(permission)) {
  return res.status(403).json({ error: 'Insufficient permissions' });
}

next();

}; }

// Apply permission checks app.post('/api/articles', authenticateToken, checkPermission('articles:create'), (req, res) => { // Create article logic } );

app.delete('/api/articles/:id', authenticateToken, checkPermission('articles:delete'), (req, res) => { // Delete article logic } );

API Input Validation

Comprehensive Request Validation

Using Express validator for Node.js:

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

app.post('/api/users', // Validate request body [ body('username') .isLength({ min: 3, max: 50 }).withMessage('Username must be 3-50 characters') .isAlphanumeric().withMessage('Username must contain only letters and numbers'),

body('email')
  .isEmail().withMessage('Must provide a valid email')
  .normalizeEmail(),

body('role')
  .isIn(['admin', 'user', 'manager']).withMessage('Invalid role specified')
  .optional(),

body('settings.*').optional() // Allow any settings object structure

], (req, res) => { // Check for validation errors const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

// Process valid input
createUser(req.body);
res.status(201).json({ message: 'User created successfully' });

} );

app.get('/api/products', // Validate query parameters [ query('page') .optional() .isInt({ min: 1 }).withMessage('Page must be a positive integer') .toInt(),

query('limit')
  .optional()
  .isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1-100')
  .toInt(),

query('sort')
  .optional()
  .isIn(['name', 'price', 'date']).withMessage('Invalid sort field')

], (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

// Use validated and sanitized query params
const { page = 1, limit = 20, sort = 'date' } = req.query;
const products = getProducts(page, limit, sort);

res.json({ products });

} );

app.get('/api/products/:id', // Validate URL parameters param('id').isInt().withMessage('Product ID must be an integer'), (req, res) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); }

const product = getProductById(req.params.id);
if (!product) {
  return res.status(404).json({ error: 'Product not found' });
}

res.json({ product });

} );

API Data Filtering and Sanitization

// Sanitize and filter sensitive data
app.get('/api/users/:id', authenticateToken, async (req, res) => {
  const user = await getUserById(req.params.id);

if (!user) { return res.status(404).json({ error: 'User not found' }); }

// Filter out sensitive data based on requesting user's role const isAdmin = req.user.role === 'admin'; const isSameUser = req.user.id === user.id;

// Create sanitized response const sanitizedUser = { id: user.id, username: user.username, email: isSameUser || isAdmin ? user.email : undefined, role: isAdmin ? user.role : undefined, createdAt: user.createdAt, // Only include personal details if admin or same user ...(isSameUser || isAdmin ? { personalDetails: user.personalDetails } : {}) };

res.json({ user: sanitizedUser }); });

Rate Limiting and Resource Protection

API Rate Limiting

Using Express Rate Limit:

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

// Global rate limiter - applies to all endpoints const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the RateLimit-* headers legacyHeaders: false, // Disable the X-RateLimit-* headers message: { error: 'Too many requests, please try again later.' } });

// Apply global rate limiting app.use(globalLimiter);

// Create specific limiters for sensitive endpoints const authLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 5, // 5 failed attempts per hour standardHeaders: true, skipSuccessfulRequests: true, // Only count failed requests message: { error: 'Too many failed login attempts, please try again later.' } });

// Apply to login endpoint app.post('/api/login', authLimiter, loginHandler);

// Create tier-based rate limits const freeTierLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window max: 100, // 100 requests per hour keyGenerator: (req) => req.user.id, // Rate limit by user ID, not IP standardHeaders: true, message: { error: 'Rate limit exceeded for free tier.' } });

const premiumTierLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour window max: 1000, // 1000 requests per hour keyGenerator: (req) => req.user.id, standardHeaders: true, message: { error: 'Rate limit exceeded for premium tier.' } });

// Dynamic rate limiting based on user tier app.use('/api/data', authenticateToken, (req, res, next) => { if (req.user.tier === 'premium') { premiumTierLimiter(req, res, next); } else { freeTierLimiter(req, res, next); } });

Payload Size Limits

// Limit JSON body size
app.use(express.json({ limit: '10kb' }));

// Limit URL-encoded data app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// Custom file upload limits per route const multer = require('multer'); const upload = multer({ limits: { fileSize: 5 * 1024 * 1024 // 5MB } });

app.post('/api/upload/avatar', authenticateToken, upload.single('avatar'), (req, res) => { // Handle avatar upload } );

app.post('/api/upload/document', authenticateToken, upload.single('document'), (req, res) => { // Handle document upload } );

Secure Response Handling

HTTP Security Headers

const helmet = require('helmet');

// Apply security headers to all routes app.use(helmet());

// Configure specific headers app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "trusted-scripts.com"], objectSrc: ["'none'"], upgradeInsecureRequests: [] } }, crossOriginResourcePolicy: { policy: "same-site" }, crossOriginOpenerPolicy: { policy: "same-origin" } }));

// Add custom security headers app.use((req, res, next) => { res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); next(); });

CORS Configuration

const cors = require('cors');

// Basic CORS setup const corsOptions = { origin: 'https://yourapplication.com', // Restrict to your domain methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed methods allowedHeaders: ['Content-Type', 'Authorization'], // Allowed headers exposedHeaders: ['Content-Range', 'X-Total-Count'], // Expose these headers credentials: true, // Allow cookies maxAge: 86400 // Cache preflight requests for 24 hours };

app.use(cors(corsOptions));

// Dynamic CORS based on environment if (process.env.NODE_ENV === 'development') { app.use(cors({ origin: '*', // Allow any origin in development credentials: true })); } else { app.use(cors({ origin: [ 'https://production-app.com', 'https://admin.production-app.com' ], credentials: true })); }

// Route-specific CORS app.options('/api/special-endpoint', cors({ origin: 'https://partner-site.com', methods: ['POST'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header'] }));

app.post('/api/special-endpoint', cors({ origin: 'https://partner-site.com', methods: ['POST'], allowedHeaders: ['Content-Type', 'Authorization', 'X-Special-Header'] }), (req, res) => { // Handle special endpoint } );

API Logging and Monitoring

Secure API Logging

const winston = require('winston');
const expressWinston = require('express-winston');

// Create sanitized transport const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports:

Clone this wiki locally