Why API Security Matters
APIs are the backbone of modern applications, but they're also prime targets for attackers. A single vulnerable endpoint can compromise your entire system. Implementing robust security measures is not optional—it's essential for protecting your data, users, and business reputation.
Common API Security Threats
- Injection Attacks: SQL injection, NoSQL injection, command injection
- Broken Authentication: Weak credentials, session management flaws
- Sensitive Data Exposure: Unencrypted data transmission, weak encryption
- XML External Entities (XXE): XML processing vulnerabilities
- Broken Access Control: Inadequate authorization checks
- Security Misconfiguration: Default settings, unnecessary features
- Cross-Site Scripting (XSS): Client-side code injection
- Insecure Deserialization: Object injection vulnerabilities
- Insufficient Logging & Monitoring: Inadequate security monitoring
Authentication & Authorization
1. Implement Strong Authentication
Use industry-standard authentication methods:
// JWT-based authentication example
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// Login endpoint
app.post('/api/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validate input
if (!email || !password) {
return res.status(400).json({ error: 'Email and password required' });
}
// Find user and verify password
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token, user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
2. Implement Proper Authorization
// Role-based access control middleware
const authorize = (roles) => {
return (req, res, next) => {
const userRole = req.user.role;
if (!roles.includes(userRole)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Usage
app.get('/api/admin/users', authenticateToken, authorize(['admin']), getUsers);
app.put('/api/users/:id', authenticateToken, authorize(['admin', 'user']), updateUser);
3. Use OAuth 2.0 and OpenID Connect
// OAuth 2.0 implementation with Passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/api/auth/google/callback"
}, async (accessToken, refreshToken, profile, done) => {
// Handle user creation/authentication
const user = await findOrCreateUser(profile);
return done(null, user);
}));
// OAuth routes
app.get('/api/auth/google', passport.authenticate('google', {
scope: ['profile', 'email']
}));
app.get('/api/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
// Generate JWT and redirect
const token = jwt.sign({ userId: req.user.id }, process.env.JWT_SECRET);
res.redirect(`/dashboard?token=${token}`);
}
);
Input Validation & Sanitization
1. Validate All Input
// Using Joi for input validation
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(120),
password: Joi.string().min(8).pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/).required()
});
// Validation middleware
const validateUser = (req, res, next) => {
const { error } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details[0].message
});
}
next();
};
app.post('/api/users', validateUser, createUser);
2. Sanitize Input Data
// Using express-validator for sanitization
const { body, validationResult } = require('express-validator');
const sanitizeInput = [
body('name').trim().escape(),
body('email').normalizeEmail(),
body('description').trim().escape(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
3. Prevent SQL Injection
// Using parameterized queries (Prisma example)
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// ❌ Vulnerable to SQL injection
// const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = '${email}'`;
// ✅ Safe parameterized query
const users = await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;
// Or using Prisma's type-safe queries
const user = await prisma.user.findUnique({
where: { email: email }
});
Rate Limiting & Throttling
1. Implement Rate Limiting
// Using express-rate-limit
const rateLimit = require('express-rate-limit');
// General rate limiting
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
// Strict rate limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // limit each IP to 5 requests per windowMs
message: 'Too many authentication attempts, please try again later.',
skipSuccessfulRequests: true,
});
app.use('/api/', generalLimiter);
app.use('/api/auth/', authLimiter);
2. Advanced Rate Limiting with Redis
// Using express-rate-limit with Redis store
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT,
password: process.env.REDIS_PASSWORD
});
const redisLimiter = rateLimit({
store: new RedisStore({
client: client,
prefix: 'rl:',
}),
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests, please try again later.'
});
Data Protection & Encryption
1. Use HTTPS Everywhere
// Force HTTPS in production
const helmet = require('helmet');
app.use(helmet({
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Redirect HTTP to HTTPS
if (process.env.NODE_ENV === 'production') {
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`);
} else {
next();
}
});
}
2. Encrypt Sensitive Data
// Encrypting sensitive data at rest
const crypto = require('crypto');
class DataEncryption {
constructor(secretKey) {
this.algorithm = 'aes-256-gcm';
this.secretKey = crypto.scryptSync(secretKey, 'salt', 32);
}
encrypt(text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, this.secretKey);
cipher.setAAD(Buffer.from('additional data'));
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return {
encrypted,
iv: iv.toString('hex'),
authTag: authTag.toString('hex')
};
}
decrypt(encryptedData) {
const decipher = crypto.createDecipher(
this.algorithm,
this.secretKey
);
decipher.setAAD(Buffer.from('additional data'));
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
let decrypted = decipher.update(encryptedData.encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
API Security Headers
Essential Security Headers
// Using helmet for security headers
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
crossOriginEmbedderPolicy: false,
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Custom security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
Error Handling & Logging
1. Secure Error Handling
// Centralized error handling
const errorHandler = (err, req, res, next) => {
// Log error details
console.error('Error:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
// Don't expose internal errors to client
if (err.status) {
res.status(err.status).json({
error: err.message,
timestamp: new Date().toISOString()
});
} else {
res.status(500).json({
error: 'Internal server error',
timestamp: new Date().toISOString()
});
}
};
app.use(errorHandler);
2. Security Monitoring & Logging
// Security event logging
const winston = require('winston');
const securityLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'security.log' }),
new winston.transports.Console()
]
});
// Log security events
const logSecurityEvent = (event, req, details = {}) => {
securityLogger.info({
event,
ip: req.ip,
userAgent: req.get('User-Agent'),
url: req.url,
method: req.method,
userId: req.user?.id,
timestamp: new Date().toISOString(),
...details
});
};
// Usage
app.post('/api/auth/login', (req, res, next) => {
// ... authentication logic
if (loginSuccessful) {
logSecurityEvent('LOGIN_SUCCESS', req, { userId: user.id });
} else {
logSecurityEvent('LOGIN_FAILED', req, { reason: 'Invalid credentials' });
}
});
API Versioning & Deprecation
Version Your APIs
// API versioning strategies
// URL versioning
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// Header versioning
app.use((req, res, next) => {
const apiVersion = req.headers['api-version'] || 'v1';
req.apiVersion = apiVersion;
next();
});
// Deprecation headers
app.use('/api/v1', (req, res, next) => {
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', '2025-12-31T23:59:59Z');
res.setHeader('Link', '; rel="successor-version"');
next();
});
Testing & Security Auditing
1. Automated Security Testing
// Using OWASP ZAP for security testing
const zap = require('zaproxy');
// Security test configuration
const securityTests = {
sqlInjection: true,
xss: true,
csrf: true,
authentication: true,
authorization: true
};
// Run security tests
async function runSecurityTests() {
const zap = new zap('localhost', 8080);
await zap.spider.scan('http://localhost:3000');
await zap.activeScan.scan('http://localhost:3000');
const alerts = await zap.core.alerts();
return alerts;
}
2. Dependency Scanning
// Using npm audit for dependency vulnerabilities
// package.json scripts
{
"scripts": {
"audit": "npm audit",
"audit:fix": "npm audit fix",
"security:check": "npm audit --audit-level moderate"
}
}
// Using Snyk for advanced vulnerability scanning
// npx snyk test
// npx snyk monitor
Security Checklist
- ✅ Implement strong authentication (JWT, OAuth 2.0)
- ✅ Use proper authorization and RBAC
- ✅ Validate and sanitize all input data
- ✅ Implement rate limiting and throttling
- ✅ Use HTTPS everywhere
- ✅ Encrypt sensitive data at rest and in transit
- ✅ Set proper security headers
- ✅ Implement comprehensive logging and monitoring
- ✅ Use parameterized queries to prevent SQL injection
- ✅ Implement proper error handling
- ✅ Regular security testing and auditing
- ✅ Keep dependencies updated
- ✅ Implement API versioning
- ✅ Use CORS properly
- ✅ Implement request/response validation
Test Your API Security
Use our OIDC playground and JWT verification tools to test and validate your API authentication and security implementations.
Use OIDC Playground Use JWT Verifier