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 1000+ 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
Recommended Reading
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 |
How Viprasol Helps
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 →
Related Topics
- API Development Company
- GraphQL API Development
- SaaS Security Best Practices
- Software Testing Company
External Resources
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 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
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.