APIs are the primary attack surface for modern applications. Securing them requires a defense-in-depth approach covering authentication, authorization, input validation, and monitoring. This guide covers practical security implementations for 2026.
Authentication Best Practices
// Secure JWT implementation
import { SignJWT, jwtVerify, JWTPayload } from 'jose';
import { createHash, randomBytes } from 'crypto';
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!);
const ACCESS_TOKEN_EXPIRY = '15m';
const REFRESH_TOKEN_EXPIRY = '7d';
interface TokenPayload extends JWTPayload {
sub: string;
email: string;
roles: string[];
tokenVersion: number;
}
async function generateAccessToken(user: User): Promise<string> {
return new SignJWT({
sub: user.id,
email: user.email,
roles: user.roles,
tokenVersion: user.tokenVersion,
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setExpirationTime(ACCESS_TOKEN_EXPIRY)
.setIssuer('your-app')
.setAudience('your-api')
.sign(JWT_SECRET);
}
async function generateRefreshToken(userId: string): Promise<string> {
const token = randomBytes(32).toString('hex');
const hash = createHash('sha256').update(token).digest('hex');
// Store hash in database with expiry
await db.refreshToken.create({
data: {
userId,
tokenHash: hash,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
},
});
return token;
}
async function verifyAccessToken(token: string): Promise<TokenPayload> {
try {
const { payload } = await jwtVerify(token, JWT_SECRET, {
issuer: 'your-app',
audience: 'your-api',
});
// Verify token version hasn't been revoked
const user = await db.user.findUnique({ where: { id: payload.sub } });
if (!user || user.tokenVersion !== payload.tokenVersion) {
throw new Error('Token revoked');
}
return payload as TokenPayload;
} catch (error) {
throw new Error('Invalid token');
}
}
// Revoke all tokens for a user
async function revokeAllTokens(userId: string): Promise<void> {
await db.user.update({
where: { id: userId },
data: { tokenVersion: { increment: 1 } },
});
await db.refreshToken.deleteMany({ where: { userId } });
}Rate Limiting
// Advanced rate limiting with sliding window
import { Redis } from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
keyPrefix: string;
}
const rateLimitConfigs: Record<string, RateLimitConfig> = {
default: { windowMs: 60000, maxRequests: 100, keyPrefix: 'rl:default' },
auth: { windowMs: 300000, maxRequests: 10, keyPrefix: 'rl:auth' },
sensitive: { windowMs: 60000, maxRequests: 20, keyPrefix: 'rl:sensitive' },
};
async function checkRateLimit(
identifier: string,
configName: string = 'default'
): Promise<{ allowed: boolean; remaining: number; resetIn: number }> {
const config = rateLimitConfigs[configName];
const key = `${config.keyPrefix}:${identifier}`;
const now = Date.now();
const windowStart = now - config.windowMs;
// Sliding window using sorted set
const pipeline = redis.pipeline();
// Remove old entries
pipeline.zremrangebyscore(key, 0, windowStart);
// Count current requests
pipeline.zcard(key);
// Add current request
pipeline.zadd(key, now, `${now}-${Math.random()}`);
// Set TTL
pipeline.pexpire(key, config.windowMs);
const results = await pipeline.exec();
const currentCount = results![1][1] as number;
if (currentCount >= config.maxRequests) {
// Get oldest entry to calculate reset time
const oldest = await redis.zrange(key, 0, 0, 'WITHSCORES');
const resetIn = oldest.length > 1
? parseInt(oldest[1]) + config.windowMs - now
: config.windowMs;
return {
allowed: false,
remaining: 0,
resetIn,
};
}
return {
allowed: true,
remaining: config.maxRequests - currentCount - 1,
resetIn: config.windowMs,
};
}
// Middleware
export async function rateLimitMiddleware(
req: Request,
configName: string = 'default'
) {
const identifier = req.headers.get('x-user-id') ||
req.headers.get('x-forwarded-for') ||
'anonymous';
const result = await checkRateLimit(identifier, configName);
const headers = {
'X-RateLimit-Remaining': result.remaining.toString(),
'X-RateLimit-Reset': Math.ceil(result.resetIn / 1000).toString(),
};
if (!result.allowed) {
return new Response(
JSON.stringify({ error: 'Too many requests' }),
{ status: 429, headers }
);
}
return { headers };
}Input Validation
// Comprehensive input validation with Zod
import { z } from 'zod';
import DOMPurify from 'isomorphic-dompurify';
// Sanitization helpers
const sanitizeString = (str: string): string => {
return DOMPurify.sanitize(str.trim(), { ALLOWED_TAGS: [] });
};
const sanitizeHtml = (html: string): string => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href'],
});
};
// Custom Zod schemas
const safeString = z.string().transform(sanitizeString);
const safeHtml = z.string().transform(sanitizeHtml);
const emailSchema = z
.string()
.email()
.max(254)
.toLowerCase()
.transform(sanitizeString);
const passwordSchema = z
.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[a-z]/, 'Must contain lowercase letter')
.regex(/[A-Z]/, 'Must contain uppercase letter')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^a-zA-Z0-9]/, 'Must contain special character');
const uuidSchema = z.string().uuid();
// API request schemas
export const createUserSchema = z.object({
email: emailSchema,
password: passwordSchema,
name: safeString.min(2).max(100),
bio: safeHtml.max(1000).optional(),
});
export const updateProfileSchema = z.object({
name: safeString.min(2).max(100).optional(),
bio: safeHtml.max(1000).optional(),
avatar: z.string().url().optional(),
}).refine(
(data) => Object.keys(data).length > 0,
{ message: 'At least one field must be provided' }
);
// Query parameter validation
export const paginationSchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
order: z.enum(['asc', 'desc']).default('desc'),
});
// Middleware
export function validateBody<T>(schema: z.ZodSchema<T>) {
return async (req: Request): Promise<T> => {
const body = await req.json();
const result = schema.safeParse(body);
if (!result.success) {
throw new ValidationError(result.error.flatten());
}
return result.data;
};
}Security Headers
// Security headers middleware
export function securityHeaders(response: Response): Response {
const headers = new Headers(response.headers);
// Prevent clickjacking
headers.set('X-Frame-Options', 'DENY');
// Prevent MIME sniffing
headers.set('X-Content-Type-Options', 'nosniff');
// XSS protection (legacy, but still useful)
headers.set('X-XSS-Protection', '1; mode=block');
// Referrer policy
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Content Security Policy
headers.set('Content-Security-Policy', [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '));
// Strict Transport Security
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Permissions Policy
headers.set('Permissions-Policy', [
'camera=()',
'microphone=()',
'geolocation=()',
'interest-cohort=()',
].join(', '));
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers,
});
}OWASP API Security Checklist
API Security Checklist
Authentication:
- Use strong, rotating secrets for JWT
- Implement token refresh with rotation
- Add brute force protection
- Support MFA for sensitive operations
Authorization:
- Implement RBAC or ABAC
- Validate permissions on every request
- Use principle of least privilege
Input:
- Validate all input with strict schemas
- Sanitize HTML to prevent XSS
- Use parameterized queries
Rate Limiting:
- Implement per-user and per-IP limits
- Use sliding window algorithm
- Return proper 429 responses
Monitoring:
- Log all authentication events
- Alert on suspicious patterns
- Implement security audit trails
Conclusion
API security requires multiple layers of defense. Implement strong authentication, validate all input, rate limit aggressively, and monitor continuously. These practices form the foundation of a secure API.
Need a security audit for your APIs? Contact Jishu Labs for expert security consulting and penetration testing.
About James Wilson
James Wilson is a Security Architect at Jishu Labs specializing in application security and secure development practices.