Every API endpoint is a potential attack vector. Unsecured endpoints lead to data breaches, unauthorized access, and system compromise.
Here's how to secure every endpoint in your API.
Layer 1: Transport Security
Always Use HTTPS
HTTP transmits data in plain text. Anyone on the network can read it.
Enforce HTTPS:
app.use((req, res, next) => {
if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(301, `https://${req.hostname}${req.url}`);
}
next();
});
Set HSTS Header:
app.use((req, res, next) => {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
This tells browsers to always use HTTPS for your domain.
Use TLS 1.2 or Higher
Disable older TLS versions:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('private-key.pem'),
cert: fs.readFileSync('certificate.pem'),
minVersion: 'TLSv1.2', // Minimum TLS 1.2
ciphers: [
'ECDHE-ECDSA-AES128-GCM-SHA256',
'ECDHE-RSA-AES128-GCM-SHA256',
'ECDHE-ECDSA-AES256-GCM-SHA384',
'ECDHE-RSA-AES256-GCM-SHA384'
].join(':')
};
https.createServer(options, app).listen(443);
Layer 2: Authentication
Require Authentication
Every endpoint should verify the caller's identity:
async function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
error: 'Authentication required',
message: 'Provide credentials in the Authorization header'
});
}
try {
const token = authHeader.replace('Bearer ', '');
const decoded = jwt.verify(token, JWT_SECRET);
req.auth = {
userId: decoded.userId,
scopes: decoded.scopes || []
};
next();
} catch (error) {
return res.status(401).json({
error: 'Invalid token',
message: error.message
});
}
}
// Apply to all routes
app.use('/v1/', authenticate);
Public Endpoints
If you have public endpoints, be explicit:
// Public endpoint (no auth required)
app.get('/v1/public/pets', async (req, res) => {
const pets = await getPublicPets();
res.json(pets);
});
// Protected endpoint (auth required)
app.get('/v1/pets', authenticate, async (req, res) => {
const pets = await getUserPets(req.auth.userId);
res.json(pets);
});
Layer 3: Authorization
Authentication tells you who the user is. Authorization tells you what they can do.
Check Resource Ownership
app.get('/v1/pets/:id', authenticate, async (req, res) => {
const pet = await db.pets.findById(req.params.id);
if (!pet) {
return res.status(404).json({ error: 'Pet not found' });
}
// Verify ownership
if (pet.ownerId !== req.auth.userId) {
return res.status(403).json({
error: 'Access denied',
message: 'You do not own this pet'
});
}
res.json(pet);
});
Check Permissions (Scopes)
function requireScopes(...requiredScopes) {
return (req, res, next) => {
const userScopes = req.auth.scopes || [];
const hasAllScopes = requiredScopes.every(scope =>
userScopes.includes(scope)
);
if (!hasAllScopes) {
return res.status(403).json({
error: 'Insufficient permissions',
required: requiredScopes,
granted: userScopes
});
}
next();
};
}
// Usage
app.post('/v1/pets', authenticate, requireScopes('write:pets'), async (req, res) => {
// Only users with write:pets scope can create pets
});
app.delete('/v1/pets/:id', authenticate, requireScopes('delete:pets'), async (req, res) => {
// Only users with delete:pets scope can delete pets
});
Role-Based Access Control
function requireRole(...allowedRoles) {
return async (req, res, next) => {
const user = await db.users.findById(req.auth.userId);
if (!allowedRoles.includes(user.role)) {
return res.status(403).json({
error: 'Access denied',
message: `This endpoint requires one of: ${allowedRoles.join(', ')}`
});
}
next();
};
}
// Admin-only endpoint
app.delete('/v1/admin/users/:id', authenticate, requireRole('admin'), async (req, res) => {
await db.users.delete(req.params.id);
res.status(204).send();
});
Layer 4: Input Validation
Validate All Input
const { body, param, query, validationResult } = require('express-validator');
app.post('/v1/pets',
authenticate,
requireScopes('write:pets'),
// Validate request body
body('name').isString().trim().isLength({ min: 1, max: 100 }),
body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
body('breed').optional().isString().trim().isLength({ max: 100 }),
body('age').optional().isInt({ min: 0, max: 30 }),
body('weight').optional().isFloat({ min: 0, max: 500 }),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: 'Validation failed',
details: errors.array()
});
}
const pet = await createPet(req.auth.userId, req.body);
res.status(201).json(pet);
}
);
Sanitize Input
const sanitizeHtml = require('sanitize-html');
function sanitizeInput(data) {
if (typeof data === 'string') {
return sanitizeHtml(data, {
allowedTags: [],
allowedAttributes: {}
});
}
if (Array.isArray(data)) {
return data.map(sanitizeInput);
}
if (typeof data === 'object' && data !== null) {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
sanitized[key] = sanitizeInput(value);
}
return sanitized;
}
return data;
}
app.use(express.json());
app.use((req, res, next) => {
if (req.body) {
req.body = sanitizeInput(req.body);
}
next();
});
Layer 5: Rate Limiting
Global Rate Limit
const rateLimit = require('express-rate-limit');
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 1000, // 1000 requests per window
message: { error: 'Too many requests' }
});
app.use('/v1/', globalLimiter);
Endpoint-Specific Limits
const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.post('/v1/auth/login', strictLimiter, async (req, res) => {
// Only 5 failed login attempts per 15 minutes
});
Layer 6: Output Filtering
Remove Sensitive Fields
function sanitizeUser(user) {
const { passwordHash, apiKeys, internalNotes, ...safeUser } = user;
return safeUser;
}
app.get('/v1/users/:id', authenticate, async (req, res) => {
const user = await db.users.findById(req.params.id);
res.json(sanitizeUser(user));
});
Field-Level Permissions
function filterFields(data, allowedFields) {
const filtered = {};
allowedFields.forEach(field => {
if (data[field] !== undefined) {
filtered[field] = data[field];
}
});
return filtered;
}
app.get('/v1/users/:id', authenticate, async (req, res) => {
const user = await db.users.findById(req.params.id);
const isOwnProfile = user.id === req.auth.userId;
const isAdmin = req.auth.scopes.includes('admin:all');
let allowedFields;
if (isAdmin) {
allowedFields = ['id', 'name', 'email', 'role', 'createdAt', 'lastLoginAt'];
} else if (isOwnProfile) {
allowedFields = ['id', 'name', 'email', 'createdAt'];
} else {
allowedFields = ['id', 'name'];
}
res.json(filterFields(user, allowedFields));
});
Layer 7: Security Headers
Set Security Headers
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Additional 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();
});
Layer 8: Logging and Monitoring
Log Security Events
function logSecurityEvent(event, req, details = {}) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
event: event,
ip: req.ip,
userId: req.auth?.userId,
endpoint: req.path,
method: req.method,
userAgent: req.headers['user-agent'],
...details
}));
}
// Log authentication failures
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
logSecurityEvent('AUTH_FAILED', req, { reason: err.message });
}
next(err);
});
// Log authorization failures
function requireScopes(...scopes) {
return (req, res, next) => {
if (!hasScopes(req.auth, scopes)) {
logSecurityEvent('AUTHZ_FAILED', req, { requiredScopes: scopes });
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Monitor Suspicious Activity
async function detectSuspiciousActivity(req) {
const userId = req.auth?.userId || req.ip;
const key = `suspicious:${userId}`;
// Track failed attempts
const failedAttempts = await redis.incr(key);
await redis.expire(key, 3600); // 1 hour window
if (failedAttempts > 10) {
logSecurityEvent('SUSPICIOUS_ACTIVITY', req, {
failedAttempts: failedAttempts,
action: 'BLOCKED'
});
return true; // Block request
}
return false;
}
Complete Endpoint Security Example
app.post('/v1/pets',
// Layer 1: HTTPS (enforced globally)
// Layer 2: Authentication
authenticate,
// Layer 3: Authorization
requireScopes('write:pets'),
// Layer 4: Input validation
body('name').isString().trim().isLength({ min: 1, max: 100 }),
body('species').isIn(['DOG', 'CAT', 'BIRD', 'RABBIT']),
// Layer 5: Rate limiting
rateLimit({ windowMs: 60000, max: 10 }),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
try {
const pet = await createPet(req.auth.userId, req.body);
// Layer 6: Output filtering
const safePet = sanitizePet(pet);
// Layer 8: Logging
logSecurityEvent('PET_CREATED', req, { petId: pet.id });
res.status(201).json(safePet);
} catch (error) {
logSecurityEvent('PET_CREATE_FAILED', req, { error: error.message });
res.status(500).json({ error: 'Internal server error' });
}
}
);
Security Checklist
- ✓ HTTPS enforced
- ✓ Authentication required
- ✓ Authorization checked
- ✓ Input validated
- ✓ Input sanitized
- ✓ Rate limiting applied
- ✓ Output filtered
- ✓ Security headers set
- ✓ Events logged
- ✓ Errors handled safely
Every endpoint should pass this checklist.
Conclusion
Securing API endpoints requires multiple layers of defense. No single technique is enough. Combine authentication, authorization, validation, rate limiting, and monitoring for comprehensive security.
Start with the basics (HTTPS, authentication) and add layers progressively. Test each layer to ensure it works correctly.
Security is not a feature you add at the end—it's a foundation you build from the start.