API Security Best Practices: Authentication, Authorization, and OWASP Top 10
API security best practices in 2026 — JWT authentication, OAuth 2.0, rate limiting, input validation, OWASP API Security Top 10, and production-grade TypeScript
API Security Best Practices: Authentication, Authorization, and OWASP Top 10
APIs are the most attacked surface in modern applications. The 2024 Verizon Data Breach Investigations Report found that APIs were involved in 83% of web application breaches. Most of those breaches exploited issues that are entirely preventable with standard techniques.
This guide covers the full API security stack: authentication patterns, authorization design, input validation, rate limiting, and the OWASP API Security Top 10 with practical mitigation code for each.
Authentication: JWT vs. Sessions vs. API Keys
JWT (JSON Web Tokens)
JWTs are signed tokens that contain claims. The server doesn't need to store session state — the token is self-contained.
import jwt from 'jsonwebtoken';
import { z } from 'zod';
const JWT_SECRET = process.env.JWT_SECRET!; // Must be ≥32 bytes, rotated regularly
const ACCESS_TOKEN_TTL = '15m'; // Short-lived — this is critical
const REFRESH_TOKEN_TTL = '7d';
interface TokenPayload {
sub: string; // User ID
email: string;
role: string;
iat: number; // Issued at
exp: number; // Expiry
}
// Issue token pair on login
function issueTokens(userId: string, email: string, role: string) {
const accessToken = jwt.sign(
{ sub: userId, email, role },
JWT_SECRET,
{ expiresIn: ACCESS_TOKEN_TTL, algorithm: 'HS256' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_TTL, algorithm: 'HS256' }
);
return { accessToken, refreshToken };
}
// Verify and decode
function verifyAccessToken(token: string): TokenPayload {
try {
return jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] }) as TokenPayload;
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
throw new Error('TOKEN_EXPIRED');
}
throw new Error('TOKEN_INVALID');
}
}
// Auth middleware
export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
}
const token = authHeader.slice(7);
try {
const payload = verifyAccessToken(token);
req.user = payload;
next();
} catch (err: any) {
const status = err.message === 'TOKEN_EXPIRED' ? 401 : 401;
res.status(status).json({ error: err.message });
}
};
JWT security rules:
- Access tokens expire in 15 minutes — not hours, not days
- Refresh tokens expire in 7 days and are stored in an HTTP-only cookie (not localStorage)
- Use
HS256(HMAC) orRS256(RSA) — nevernone - Rotate the JWT secret periodically; have a rotation procedure
API Key Authentication
For machine-to-machine and partner integrations:
import crypto from 'crypto';
// Generate API key
function generateApiKey(): { key: string; hash: string } {
const key = `vip_${crypto.randomBytes(32).toString('hex')}`;
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash }; // Store hash in DB, return key once to user
}
// Verify API key (hash comparison — no plain-text storage)
async function verifyApiKey(providedKey: string): Promise<ApiClient | null> {
const hash = crypto.createHash('sha256').update(providedKey).digest('hex');
const client = await db('api_keys')
.where({ key_hash: hash, revoked: false })
.first();
if (!client) return null;
// Update last used (async — don't block the response)
db('api_keys').where({ id: client.id }).update({ last_used_at: new Date() }).catch(console.error);
return client;
}
Authorization: RBAC and Resource-Level Checks
Authentication answers "who are you?" Authorization answers "what can you do?"
Role-Based Access Control (RBAC)
type Permission =
| 'users:read'
| 'users:write'
| 'billing:read'
| 'billing:write'
| 'admin:all';
const ROLE_PERMISSIONS: Record<string, Permission[]> = {
viewer: ['users:read'],
member: ['users:read', 'users:write'],
billing_admin: ['users:read', 'billing:read', 'billing:write'],
admin: ['users:read', 'users:write', 'billing:read', 'billing:write', 'admin:all'],
};
function can(userRole: string, permission: Permission): boolean {
return ROLE_PERMISSIONS[userRole]?.includes(permission) ?? false;
}
// Authorization middleware factory
function requirePermission(permission: Permission) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Unauthenticated' });
if (!can(req.user.role, permission)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
router.get('/admin/users', authenticate, requirePermission('admin:all'), listUsers);
router.post('/users/:id', authenticate, requirePermission('users:write'), updateUser);
Resource-Level Authorization (CRITICAL — Prevents BOLA)
Always verify the requesting user owns or has access to the specific resource:
// ❌ BAD: Only checks authentication, not ownership
app.get('/orders/:orderId', authenticate, async (req, res) => {
const order = await db('orders').where({ id: req.params.orderId }).first();
res.json(order); // Any authenticated user can see any order!
});
// ✅ GOOD: Verify resource ownership
app.get('/orders/:orderId', authenticate, async (req, res) => {
const order = await db('orders')
.where({
id: req.params.orderId,
user_id: req.user.sub, // Scope to authenticated user's orders
})
.first();
if (!order) {
// Return 404, not 403 — don't reveal whether the resource exists
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});
This is OWASP API1:2023 — Broken Object Level Authorization (BOLA). It's the #1 API vulnerability in production systems.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
OWASP API Security Top 10 (2023) Mitigations
API1: Broken Object Level Authorization (BOLA)
Always scope queries by user_id (or equivalent). Never trust the ID in the URL alone.
API2: Broken Authentication
// Brute-force protection on login
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per IP
message: { error: 'Too many login attempts, try again in 15 minutes' },
standardHeaders: true,
legacyHeaders: false,
// Additional: lock account after N failures (track in DB)
});
app.post('/auth/login', loginLimiter, handleLogin);
API3: Broken Object Property Level Authorization
Return only the fields the user should see. Never return entire DB rows:
// ❌ BAD: Returns password_hash, internal flags, admin fields
const user = await db('users').where({ id: userId }).first();
res.json(user);
// ✅ GOOD: Explicit field selection
const user = await db('users')
.select('id', 'email', 'display_name', 'created_at', 'plan')
.where({ id: userId })
.first();
res.json(user);
API4: Unrestricted Resource Consumption
// Rate limiting per API key / user (not just per IP)
const apiRateLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute window
max: 100,
keyGenerator: (req) => req.user?.sub ?? req.ip, // Rate limit per user, not IP
handler: (req, res) => {
res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
});
},
});
// Also limit request body size
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
API5: Broken Function Level Authorization
Separate admin endpoints into a different router with explicit admin-only middleware:
// Admin router — every route requires admin role
const adminRouter = express.Router();
adminRouter.use(authenticate);
adminRouter.use(requirePermission('admin:all'));
adminRouter.get('/users', listAllUsers);
adminRouter.delete('/users/:id', deleteUser);
adminRouter.post('/users/:id/ban', banUser);
// Regular router — user-scoped endpoints
app.use('/api/v1', userRouter);
app.use('/api/v1/admin', adminRouter); // Separate prefix
API8: Security Misconfiguration
// Security headers — use helmet
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: true,
hsts: { maxAge: 31536000, includeSubDomains: true },
noSniff: true,
xssFilter: true,
}));
// CORS — explicit whitelist, never wildcard for authenticated APIs
import cors from 'cors';
const allowedOrigins = [
'https://app.yourproduct.com',
'https://yourproduct.com',
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000'] : []),
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS: Origin not allowed'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));
API10: Unsafe Consumption of APIs
When consuming external APIs, validate responses:
import { z } from 'zod';
const ExternalUserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string().max(255),
// Don't pass unknown fields through to your system
});
async function fetchExternalUser(id: string): Promise<ExternalUser> {
const response = await fetch(`https://partner-api.com/users/${id}`, {
headers: { Authorization: `Bearer ${process.env.PARTNER_API_KEY}` },
signal: AbortSignal.timeout(5000), // 5-second timeout
});
if (!response.ok) {
throw new Error(`Partner API error: ${response.status}`);
}
const raw = await response.json();
return ExternalUserSchema.parse(raw); // Validates AND strips unknown fields
}
Input Validation
Every external input is untrusted. Validate at the API boundary:
import { z } from 'zod';
const CreateOrderSchema = z.object({
productId: z.string().uuid(),
quantity: z.number().int().min(1).max(1000),
shippingAddress: z.object({
line1: z.string().min(1).max(200),
city: z.string().min(1).max(100),
country: z.string().length(2), // ISO country code
postalCode: z.string().regex(/^[A-Z0-9\s-]{3,10}$/i),
}),
couponCode: z.string().regex(/^[A-Z0-9]{6,12}$/).optional(),
});
app.post('/orders', authenticate, async (req, res) => {
const result = CreateOrderSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.flatten(),
});
}
const order = await orderService.create(req.user.sub, result.data);
res.status(201).json(order);
});
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Logging and Monitoring
// Log security events for audit and incident response
async function logSecurityEvent(event: {
type: 'login_failed' | 'access_denied' | 'rate_limit_hit' | 'suspicious_request';
userId?: string;
ip: string;
details: Record<string, unknown>;
}): Promise<void> {
await db('security_events').insert({
...event,
occurred_at: new Date(),
});
// Alert on high-frequency suspicious activity
if (event.type === 'login_failed') {
const recentFailures = await db('security_events')
.where({ type: 'login_failed', ip: event.ip })
.where('occurred_at', '>', new Date(Date.now() - 60000))
.count('id as count');
if (Number(recentFailures[0].count) > 10) {
await alertService.send(`Brute force attempt from IP ${event.ip}`);
}
}
}
Security Implementation Costs
| Task | Investment |
|---|---|
| API security audit | $5,000–$15,000 |
| Auth system implementation (JWT + refresh) | $5,000–$12,000 |
| RBAC + resource-level authorization | $8,000–$20,000 |
| Rate limiting + DDoS protection | $3,000–$8,000 |
| Full API security hardening | $20,000–$50,000 |
| Penetration test (external) | $5,000–$20,000 |
Working With Viprasol
We build secure APIs from the ground up — or audit and harden existing ones. Our security implementations are OWASP-aligned and include authentication, authorization, input validation, rate limiting, and monitoring.
→ API security review →
→ API Development Services →
→ Web Development Services →
See Also
- API Development Company
- GraphQL API Development
- SaaS Security Best Practices
- Software Testing Company
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.