APIs are the backbone of modern applications. They’re also the most common attack vector. Here’s how to build APIs that don’t break under pressure.
The API Security Problem
Your API is public by nature. Unlike traditional web apps where you control the interface, APIs expose raw functionality that anyone can probe. Every endpoint is a potential entry point.
Authentication Done Right
Use OAuth 2.0 / OpenID Connect
JWT tokens are standard, but implementation matters:
// Verify JWT with proper options
const verified = jwt.verify(token, publicKey, {
algorithms: ['RS256'], // Specify allowed algorithms
issuer: 'https://auth.yoursite.com',
audience: 'your-api',
clockTolerance: 30, // Allow 30 seconds of clock skew
});
Short-Lived Tokens
- Access tokens: 15 minutes max
- Refresh tokens: 7 days with rotation
- API keys: Rotate quarterly at minimum
Rate Limiting
Without rate limiting, your API is vulnerable to:
- Brute force attacks
- Denial of service
- Credential stuffing
// Example rate limiting middleware
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: { error: 'Too many requests, try again later' },
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', apiLimiter);
Input Validation
Never trust client data. Validate everything:
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']),
});
function createUser(data: unknown) {
const validated = CreateUserSchema.parse(data);
// Now safe to use
}
Authorization Patterns
Authentication answers “who are you?” Authorization answers “what can you do?”
Resource-Based Authorization
async function getDocument(userId, documentId) {
const doc = await db.documents.find(documentId);
// Check ownership or explicit sharing
if (doc.ownerId !== userId && !doc.sharedWith.includes(userId)) {
throw new ForbiddenError();
}
return doc;
}
Role-Based Access Control (RBAC)
const permissions = {
admin: ['read', 'write', 'delete', 'manage'],
editor: ['read', 'write'],
viewer: ['read'],
};
function canAccess(userRole, requiredPermission) {
return permissions[userRole]?.includes(requiredPermission) ?? false;
}
CORS Configuration
Misconfigured CORS is a common vulnerability:
// DANGEROUS - allows any origin
app.use(cors({ origin: '*' }));
// SECURE - whitelist allowed origins
app.use(cors({
origin: ['https://app.yoursite.com', 'https://admin.yoursite.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
Error Handling
Don’t leak information through errors:
// BAD - leaks implementation details
catch (error) {
res.status(500).json({ error: error.message, stack: error.stack });
}
// GOOD - generic error for clients, detailed logging server-side
catch (error) {
logger.error('Database error', { error, requestId });
res.status(500).json({ error: 'Internal server error', requestId });
}
Conclusion
API security isn’t a feature—it’s a requirement. Start with authentication, add authorization, validate everything, and monitor relentlessly. Your users are counting on it.
Hacker Bot’s API security testing finds vulnerabilities in your endpoints before attackers do. Try it on your API today.