-
Notifications
You must be signed in to change notification settings - Fork 4
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.
- Authentication flaws - Weak or missing authentication mechanisms
- Authorization issues - Improper access controls
- Input validation gaps - Accepting and processing malicious input
- Excessive data exposure - Returning more data than necessary
- Resource constraints lacking - Allowing excessive usage or abuse
- Security misconfigurations - Insecure default settings
- Injection flaws - Command or query injection vulnerabilities
- Improper assets management - Unpatched or deprecated API versions
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 |
// 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
});
});
});
// 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() });
}
);
// 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
}
);
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 });
}
);
// 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 });
});
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);
}
});
// 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
}
);
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();
});
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
}
);
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: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Sensitive data patterns to redact
const sensitiveFields = ['password', 'token', 'secret', 'authorization', 'cookie'];
// Custom sanitization function
const sanitizeRequest = (req) => {
const sanitized = {
headers: { ...req.headers },
body: { ...req.body },
query: { ...req.query }
};
// Sanitize headers
sensitiveFields.forEach(field => {
if (sanitized.headers[field]) {
sanitized.headers[field] = '[REDACTED]';
}
// Case insensitive check for Authorization header
if (field === 'authorization' && sanitized.headers['Authorization']) {
sanitized.headers['Authorization'] = '[REDACTED]';
}
});
// Sanitize body
sensitiveFields.forEach(field => {
if (sanitized.body && sanitized.body[field]) {
sanitized.body[field] = '[REDACTED]';
}
});
return sanitized;
};
// HTTP request logger
app.use(expressWinston.logger({
winstonInstance: logger,
meta: true,
// Sanitize request data before logging
dynamicMeta: (req, res) => {
return {
request: sanitizeRequest(req),
userId: req.user ? req.user.id : 'unauthenticated',
ip: req.ip
};
},
msg: 'HTTP {{req.method}} {{req.url}}',
expressFormat: true,
colorize: false
}));
// Error logger
app.use(expressWinston.errorLogger({
winstonInstance: logger,
blacklistedMetaFields: ['exception', 'process', 'os']
}));
// Using Prometheus for API monitoring
const prometheus = require('prom-client');
const register = new prometheus.Registry();
// Add default metrics
prometheus.collectDefaultMetrics({ register });
// Create custom metrics
const httpRequestDurationMicroseconds = new prometheus.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'status_code'],
buckets: [10, 50, 100, 500, 1000, 5000]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const apiErrorTotal = new prometheus.Counter({
name: 'api_errors_total',
help: 'Total number of API errors',
labelNames: ['method', 'route', 'error_code']
});
// Register custom metrics
register.registerMetric(httpRequestDurationMicroseconds);
register.registerMetric(httpRequestTotal);
register.registerMetric(apiErrorTotal);
// Middleware to record metrics
app.use((req, res, next) => {
const start = Date.now();
// Record when request ends
res.on('finish', () => {
const duration = Date.now() - start;
const route = req.route ? req.route.path : req.path;
// Record request duration
httpRequestDurationMicroseconds
.labels(req.method, route, res.statusCode)
.observe(duration);
// Count request
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
// Count errors
if (res.statusCode >= 400) {
apiErrorTotal
.labels(req.method, route, res.statusCode)
.inc();
}
});
next();
});
// Expose metrics endpoint for Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
// API versioning
const apiV1Router = express.Router();
const apiV2Router = express.Router();
// Set up v1 endpoints
apiV1Router.get('/users', (req, res) => {
// V1 implementation
res.json({ users: getUsersV1() });
});
// Set up v2 endpoints
apiV2Router.get('/users', (req, res) => {
// V2 implementation with enhanced data
res.json({
users: getUsersV2(),
pagination: {
totalCount: 100,
page: 1,
pageSize: 20
}
});
});
// Mount versioned routers
app.use('/api/v1', apiV1Router);
app.use('/api/v2', apiV2Router);
// Deprecation warning middleware for v1
app.use('/api/v1', (req, res, next) => {
res.setHeader('Warning', '299 - "Deprecated API version. Please migrate to v2 by July 2024"');
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Deprecation-Date', 'July 1, 2024');
res.setHeader('X-API-Migration-URL', 'https://api.example.com/docs/v2-migration');
next();
});
// Default to latest version
app.use('/api', apiV2Router);
const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { applyMiddleware } = require('graphql-middleware');
const { rule, shield, allow, deny } = require('graphql-shield');
// Define GraphQL schema
const typeDefs = `
type User {
id: ID!
username: String!
email: String!
role: String!
sensitiveData: String
}
type Query {
me: User
user(id: ID!): User
users: [User]
}
`;
// Define resolvers
const resolvers = {
Query: {
me: (, __, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
},
user: (, { id }, context) => {
return getUserById(id);
},
users: (_, __, context) => {
return getAllUsers();
}
}
};
// Define permission rules
const isAuthenticated = rule({ cache: 'contextual' })(
async (_, __, context) => {
return !!context.user;
}
);
const isAdmin = rule({ cache: 'contextual' })(
async (_, __, context) => {
return context.user && context.user.role === 'admin';
}
);
const isSameUserOrAdmin = rule({ cache: 'contextual' })(
async (_, args, context) => {
if (!context.user) return false;
if (context.user.role === 'admin') return true;
return context.user.id === args.id;
}
);
// Define permissions
const permissions = shield({
Query: {
me: isAuthenticated,
user: isSameUserOrAdmin,
users: isAdmin
},
User: {
email: isAuthenticated,
sensitiveData: isSameUserOrAdmin
}
});
// Create executable schema with permissions
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaWithPermissions = applyMiddleware(schema, permissions);
// Configure Apollo Server
const server = new ApolloServer({
schema: schemaWithPermissions,
context: ({ req }) => {
// Authentication logic here
const user = authenticateRequest(req);
return { user };
},
// Set limits
validationRules: [
depthLimit(5), // Limit query depth
queryComplexity({
maximumComplexity: 1000,
variables: {},
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
}
})
],
// Apollo Studio settings
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production'
});
// Apply Apollo middleware to Express
server.applyMiddleware({ app });
- OWASP API Security Top 10
- JWT Best Practices
- API Security Checklist
- GraphQL Security
- API Gateway 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.
- Authentication flaws - Weak or missing authentication mechanisms
- Authorization issues - Improper access controls
- Input validation gaps - Accepting and processing malicious input
- Excessive data exposure - Returning more data than necessary
- Resource constraints lacking - Allowing excessive usage or abuse
- Security misconfigurations - Insecure default settings
- Injection flaws - Command or query injection vulnerabilities
- Improper assets management - Unpatched or deprecated API versions
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 |
// 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
});
});
});
// 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() });
}
);
// 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
}
);
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 });
}
);
// 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 });
});
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);
}
});
// 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
}
);
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();
});
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
}
);
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: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// Sensitive data patterns to redact
const sensitiveFields = ['password', 'token', 'secret', 'authorization', 'cookie'];
// Custom sanitization function
const sanitizeRequest = (req) => {
const sanitized = {
headers: { ...req.headers },
body: { ...req.body },
query: { ...req.query }
};
// Sanitize headers
sensitiveFields.forEach(field => {
if (sanitized.headers[field]) {
sanitized.headers[field] = '[REDACTED]';
}
// Case insensitive check for Authorization header
if (field === 'authorization' && sanitized.headers['Authorization']) {
sanitized.headers['Authorization'] = '[REDACTED]';
}
});
// Sanitize body
sensitiveFields.forEach(field => {
if (sanitized.body && sanitized.body[field]) {
sanitized.body[field] = '[REDACTED]';
}
});
return sanitized;
};
// HTTP request logger
app.use(expressWinston.logger({
winstonInstance: logger,
meta: true,
// Sanitize request data before logging
dynamicMeta: (req, res) => {
return {
request: sanitizeRequest(req),
userId: req.user ? req.user.id : 'unauthenticated',
ip: req.ip
};
},
msg: 'HTTP {{req.method}} {{req.url}}',
expressFormat: true,
colorize: false
}));
// Error logger
app.use(expressWinston.errorLogger({
winstonInstance: logger,
blacklistedMetaFields: ['exception', 'process', 'os']
}));
// Using Prometheus for API monitoring
const prometheus = require('prom-client');
const register = new prometheus.Registry();
// Add default metrics
prometheus.collectDefaultMetrics({ register });
// Create custom metrics
const httpRequestDurationMicroseconds = new prometheus.Histogram({
name: 'http_request_duration_ms',
help: 'Duration of HTTP requests in ms',
labelNames: ['method', 'route', 'status_code'],
buckets: [10, 50, 100, 500, 1000, 5000]
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
const apiErrorTotal = new prometheus.Counter({
name: 'api_errors_total',
help: 'Total number of API errors',
labelNames: ['method', 'route', 'error_code']
});
// Register custom metrics
register.registerMetric(httpRequestDurationMicroseconds);
register.registerMetric(httpRequestTotal);
register.registerMetric(apiErrorTotal);
// Middleware to record metrics
app.use((req, res, next) => {
const start = Date.now();
// Record when request ends
res.on('finish', () => {
const duration = Date.now() - start;
const route = req.route ? req.route.path : req.path;
// Record request duration
httpRequestDurationMicroseconds
.labels(req.method, route, res.statusCode)
.observe(duration);
// Count request
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
// Count errors
if (res.statusCode >= 400) {
apiErrorTotal
.labels(req.method, route, res.statusCode)
.inc();
}
});
next();
});
// Expose metrics endpoint for Prometheus scraping
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});
// API versioning
const apiV1Router = express.Router();
const apiV2Router = express.Router();
// Set up v1 endpoints
apiV1Router.get('/users', (req, res) => {
// V1 implementation
res.json({ users: getUsersV1() });
});
// Set up v2 endpoints
apiV2Router.get('/users', (req, res) => {
// V2 implementation with enhanced data
res.json({
users: getUsersV2(),
pagination: {
totalCount: 100,
page: 1,
pageSize: 20
}
});
});
// Mount versioned routers
app.use('/api/v1', apiV1Router);
app.use('/api/v2', apiV2Router);
// Deprecation warning middleware for v1
app.use('/api/v1', (req, res, next) => {
res.setHeader('Warning', '299 - "Deprecated API version. Please migrate to v2 by July 2024"');
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Deprecation-Date', 'July 1, 2024');
res.setHeader('X-API-Migration-URL', 'https://api.example.com/docs/v2-migration');
next();
});
// Default to latest version
app.use('/api', apiV2Router);
const { ApolloServer } = require('apollo-server-express');
const { makeExecutableSchema } = require('@graphql-tools/schema');
const { applyMiddleware } = require('graphql-middleware');
const { rule, shield, allow, deny } = require('graphql-shield');
// Define GraphQL schema
const typeDefs = `
type User {
id: ID!
username: String!
email: String!
role: String!
sensitiveData: String
}
type Query {
me: User
user(id: ID!): User
users: [User]
}
`;
// Define resolvers
const resolvers = {
Query: {
me: (_, __, context) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return context.user;
},
user: (_, { id }, context) => {
return getUserById(id);
},
users: (_, __, context) => {
return getAllUsers();
}
}
};
// Define permission rules
const isAuthenticated = rule({ cache: 'contextual' })(
async (_, __, context) => {
return !!context.user;
}
);
const isAdmin = rule({ cache: 'contextual' })(
async (_, __, context) => {
return context.user && context.user.role === 'admin';
}
);
const isSameUserOrAdmin = rule({ cache: 'contextual' })(
async (_, args, context) => {
if (!context.user) return false;
if (context.user.role === 'admin') return true;
return context.user.id === args.id;
}
);
// Define permissions
const permissions = shield({
Query: {
me: isAuthenticated,
user: isSameUserOrAdmin,
users: isAdmin
},
User: {
email: isAuthenticated,
sensitiveData: isSameUserOrAdmin
}
});
// Create executable schema with permissions
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaWithPermissions = applyMiddleware(schema, permissions);
// Configure Apollo Server
const server = new ApolloServer({
schema: schemaWithPermissions,
context: ({ req }) => {
// Authentication logic here
const user = authenticateRequest(req);
return { user };
},
// Set limits
validationRules: [
depthLimit(5), // Limit query depth
queryComplexity({
maximumComplexity: 1000,
variables: {},
onComplete: (complexity) => {
console.log('Query complexity:', complexity);
}
})
],
// Apollo Studio settings
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production'
});
// Apply Apollo middleware to Express
server.applyMiddleware({ app });
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
- [JWT Best Practices](https://auth0.com/docs/secure/tokens/json-web-tokens/jwt-security-best-practices)
- [API Security Checklist](https://github.com/shieldfy/API-Security-Checklist)
- [GraphQL Security](https://graphql.org/learn/best-practices/#security)
- [API Gateway Best Practices](https://docs.konghq.com/gateway/latest/production/secure-admin-api/)