API Security
Authentication, authorization, input validation, and security patterns for API endpoints.
API Security
This section covers comprehensive security measures for protecting API endpoints, including authentication, authorization, input validation, and security best practices.
Security Architecture Overview
API security is implemented through multiple layers of defense:
┌─────────────────────────────────────────────────────────────┐
│ Client Request │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Rate Limiting │
│ • IP-based limits │
│ • User-based limits │
│ • Endpoint-specific limits │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Authentication │
│ • JWT token validation │
│ • Session verification │
│ • Token expiration checks │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Authorization │
│ • Role-based access control │
│ • Resource-level permissions │
│ • Context-aware authorization │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Input Validation │
│ • Schema validation │
│ • Data sanitization │
│ • Type checking │
└─────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────────────┐
│ Business Logic │
└─────────────────────────────────────────────────────────────┘
Authentication
JWT Token Authentication
// lib/auth/jwt.ts
import { SignJWT, jwtVerify } from 'jose'
import { NextRequest } from 'next/server'
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!)
export interface JWTPayload {
sub: string // user ID
email: string
role: string
iat: number
exp: number
}
export async function signJWT(payload: Omit<JWTPayload, 'iat' | 'exp'>) {
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('24h')
.sign(JWT_SECRET)
}
export async function verifyJWT(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, JWT_SECRET)
return payload as JWTPayload
}
export async function getTokenFromRequest(request: NextRequest): Promise<string | null> {
// Check Authorization header
const authHeader = request.headers.get('authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
return authHeader.substring(7)
}
// Check cookies (for web app)
const cookieToken = request.cookies.get('auth-token')?.value
if (cookieToken) {
return cookieToken
}
return null
}
Authentication Middleware
// lib/middleware/auth.ts
import { NextRequest, NextResponse } from 'next/server'
import { verifyJWT, getTokenFromRequest } from '@/lib/auth/jwt'
export async function requireAuth(request: NextRequest) {
try {
const token = await getTokenFromRequest(request)
if (!token) {
return NextResponse.json(
{ success: false, error: { code: 'UNAUTHORIZED', message: 'Authentication required' } },
{ status: 401 }
)
}
const payload = await verifyJWT(token)
// Add user info to request headers for downstream handlers
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-user-id', payload.sub)
requestHeaders.set('x-user-email', payload.email)
requestHeaders.set('x-user-role', payload.role)
return NextResponse.next({
request: {
headers: requestHeaders
}
})
} catch (error) {
return NextResponse.json(
{ success: false, error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token' } },
{ status: 401 }
)
}
}
// Helper to get current user from request
export function getCurrentUser(request: NextRequest) {
return {
id: request.headers.get('x-user-id')!,
email: request.headers.get('x-user-email')!,
role: request.headers.get('x-user-role')! as UserRole
}
}
Route Protection
// app/api/products/route.ts
import { requireAuth } from '@/lib/middleware/auth'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
// Apply authentication middleware
const authResult = await requireAuth(request)
if (authResult.status === 401) {
return authResult
}
// Get authenticated user
const user = getCurrentUser(request)
// Continue with business logic
const products = await productService.list({
// Apply user-specific filters if needed
userId: user.role === 'admin' ? undefined : user.id
})
return NextResponse.json({ success: true, data: products })
}
Authorization
Role-Based Access Control (RBAC)
// lib/auth/permissions.ts
export enum UserRole {
ADMIN = 'admin',
MANAGER = 'manager',
USER = 'user',
VIEWER = 'viewer'
}
export enum Permission {
// Product permissions
PRODUCT_CREATE = 'product:create',
PRODUCT_READ = 'product:read',
PRODUCT_UPDATE = 'product:update',
PRODUCT_DELETE = 'product:delete',
// Inventory permissions
INVENTORY_READ = 'inventory:read',
INVENTORY_UPDATE = 'inventory:update',
INVENTORY_ADJUST = 'inventory:adjust',
// Order permissions
ORDER_CREATE = 'order:create',
ORDER_READ = 'order:read',
ORDER_UPDATE = 'order:update',
ORDER_CANCEL = 'order:cancel',
// Admin permissions
USER_MANAGE = 'user:manage',
SYSTEM_CONFIG = 'system:config'
}
const rolePermissions: Record<UserRole, Permission[]> = {
[UserRole.ADMIN]: [
// All permissions
...Object.values(Permission)
],
[UserRole.MANAGER]: [
Permission.PRODUCT_CREATE,
Permission.PRODUCT_READ,
Permission.PRODUCT_UPDATE,
Permission.INVENTORY_READ,
Permission.INVENTORY_UPDATE,
Permission.INVENTORY_ADJUST,
Permission.ORDER_CREATE,
Permission.ORDER_READ,
Permission.ORDER_UPDATE,
Permission.ORDER_CANCEL
],
[UserRole.USER]: [
Permission.PRODUCT_READ,
Permission.INVENTORY_READ,
Permission.ORDER_CREATE,
Permission.ORDER_READ
],
[UserRole.VIEWER]: [
Permission.PRODUCT_READ,
Permission.INVENTORY_READ,
Permission.ORDER_READ
]
}
export function hasPermission(role: UserRole, permission: Permission): boolean {
return rolePermissions[role].includes(permission)
}
export function requirePermission(role: UserRole, permission: Permission) {
if (!hasPermission(role, permission)) {
throw new Error(`Insufficient permissions: ${permission} required`)
}
}
Authorization Middleware
// lib/middleware/authorize.ts
import { NextRequest, NextResponse } from 'next/server'
import { getCurrentUser } from './auth'
import { hasPermission, Permission } from '@/lib/auth/permissions'
export function requirePermissions(...permissions: Permission[]) {
return async function authorizationMiddleware(request: NextRequest) {
const user = getCurrentUser(request)
for (const permission of permissions) {
if (!hasPermission(user.role as UserRole, permission)) {
return NextResponse.json(
{
success: false,
error: {
code: 'FORBIDDEN',
message: `Permission required: ${permission}`
}
},
{ status: 403 }
)
}
}
return NextResponse.next()
}
}
// Usage in API routes
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
// Apply authentication
const authResult = await requireAuth(request)
if (authResult.status === 401) return authResult
// Apply authorization
const authzResult = await requirePermissions(Permission.PRODUCT_DELETE)(request)
if (authzResult.status === 403) return authzResult
// Continue with business logic
await productService.delete(params.id)
return NextResponse.json({ success: true })
}
Resource-Level Authorization
// lib/auth/resource-authorization.ts
export async function canAccessProduct(userId: string, productId: string): Promise<boolean> {
const user = await userService.getById(userId)
const product = await productService.getById(productId)
if (!product) return false
// Admin can access all products
if (user.role === UserRole.ADMIN) return true
// Users can only access products from their assigned warehouses
if (user.role === UserRole.USER) {
const userWarehouses = await userService.getAssignedWarehouses(userId)
const productWarehouse = await productService.getWarehouse(productId)
return userWarehouses.some(w => w.id === productWarehouse.id)
}
return true
}
export async function filterProductsByAccess(userId: string, products: Product[]): Promise<Product[]> {
const user = await userService.getById(userId)
if (user.role === UserRole.ADMIN) {
return products
}
if (user.role === UserRole.USER) {
const userWarehouses = await userService.getAssignedWarehouses(userId)
const warehouseIds = userWarehouses.map(w => w.id)
return products.filter(product =>
warehouseIds.includes(product.warehouse_id)
)
}
return products
}
Input Validation
Schema Validation with Zod
// lib/validation/schemas.ts
import { z } from 'zod'
export const createProductSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name too long')
.trim(),
sku: z.string()
.min(1, 'SKU is required')
.max(50, 'SKU too long')
.regex(/^[A-Z0-9-]+$/, 'SKU format invalid')
.trim()
.toUpperCase(),
price: z.number()
.positive('Price must be positive')
.max(999999.99, 'Price too high')
.transform(val => Math.round(val * 100) / 100), // Round to 2 decimals
category_id: z.string().uuid('Invalid category ID'),
description: z.string()
.max(1000, 'Description too long')
.trim()
.optional(),
barcode: z.string()
.regex(/^[0-9]+$/, 'Barcode must contain only numbers')
.min(8, 'Barcode too short')
.max(14, 'Barcode too long')
.optional(),
weight: z.number()
.positive('Weight must be positive')
.max(1000, 'Weight too high')
.optional(),
dimensions: z.object({
length: z.number().positive(),
width: z.number().positive(),
height: z.number().positive()
}).optional()
})
export const updateProductSchema = createProductSchema.partial()
export const productQuerySchema = z.object({
page: z.number().int().positive().default(1),
limit: z.number().int().positive().max(100).default(20),
search: z.string().max(100).optional(),
category_id: z.string().uuid().optional(),
in_stock: z.boolean().optional(),
sort: z.enum(['name', 'price', 'created_at', 'updated_at']).default('name'),
order: z.enum(['asc', 'desc']).default('asc')
})
Validation Middleware
// lib/middleware/validation.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
export function validateBody<T>(schema: z.ZodSchema<T>) {
return async function validationMiddleware(request: NextRequest) {
try {
const body = await request.json()
const validatedData = schema.parse(body)
// Add validated data to request
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-validated-body', JSON.stringify(validatedData))
return NextResponse.next({
request: {
headers: requestHeaders
}
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request data',
details: error.flatten().fieldErrors
}
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: {
code: 'INVALID_JSON',
message: 'Invalid JSON in request body'
}
},
{ status: 400 }
)
}
}
}
export function validateQuery<T>(schema: z.ZodSchema<T>) {
return function queryValidationMiddleware(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryData = Object.fromEntries(searchParams.entries())
// Convert string values to appropriate types
const typedQuery = Object.entries(queryData).reduce((acc, [key, value]) => {
// Try to parse as number
if (!isNaN(Number(value))) {
acc[key] = Number(value)
}
// Try to parse as boolean
else if (value === 'true' || value === 'false') {
acc[key] = value === 'true'
}
// Keep as string
else {
acc[key] = value
}
return acc
}, {} as any)
const validatedQuery = schema.parse(typedQuery)
const requestHeaders = new Headers(request.headers)
requestHeaders.set('x-validated-query', JSON.stringify(validatedQuery))
return NextResponse.next({
request: {
headers: requestHeaders
}
})
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{
success: false,
error: {
code: 'QUERY_VALIDATION_ERROR',
message: 'Invalid query parameters',
details: error.flatten().fieldErrors
}
},
{ status: 400 }
)
}
return NextResponse.json(
{
success: false,
error: {
code: 'QUERY_ERROR',
message: 'Invalid query parameters'
}
},
{ status: 400 }
)
}
}
}
Rate Limiting
Rate Limiting Implementation
// lib/rate-limit/limiter.ts
import { Redis } from 'ioredis'
const redis = new Redis(process.env.REDIS_URL!)
interface RateLimitConfig {
windowMs: number // Time window in milliseconds
maxRequests: number // Max requests per window
keyGenerator: (request: NextRequest) => string
}
export class RateLimiter {
constructor(private config: RateLimitConfig) {}
async isAllowed(request: NextRequest): Promise<{
allowed: boolean
remaining: number
resetTime: number
}> {
const key = this.config.keyGenerator(request)
const now = Date.now()
const window = Math.floor(now / this.config.windowMs)
const redisKey = `rate_limit:${key}:${window}`
const current = await redis.incr(redisKey)
if (current === 1) {
await redis.expire(redisKey, Math.ceil(this.config.windowMs / 1000))
}
const remaining = Math.max(0, this.config.maxRequests - current)
const resetTime = (window + 1) * this.config.windowMs
return {
allowed: current <= this.config.maxRequests,
remaining,
resetTime
}
}
}
// Rate limiting configurations
export const rateLimiters = {
general: new RateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 100,
keyGenerator: (req) => req.ip || 'unknown'
}),
auth: new RateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
maxRequests: 5,
keyGenerator: (req) => req.ip || 'unknown'
}),
api: new RateLimiter({
windowMs: 60 * 1000, // 1 minute
maxRequests: 60,
keyGenerator: (req) => {
const userId = req.headers.get('x-user-id')
return userId || req.ip || 'unknown'
}
})
}
Rate Limiting Middleware
// lib/middleware/rate-limit.ts
import { NextRequest, NextResponse } from 'next/server'
import { RateLimiter } from '@/lib/rate-limit/limiter'
export function applyRateLimit(limiter: RateLimiter) {
return async function rateLimitMiddleware(request: NextRequest) {
const result = await limiter.isAllowed(request)
if (!result.allowed) {
return NextResponse.json(
{
success: false,
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests. Please try again later.',
retryAfter: Math.ceil((result.resetTime - Date.now()) / 1000)
}
},
{
status: 429,
headers: {
'X-RateLimit-Remaining': result.remaining.toString(),
'X-RateLimit-Reset': result.resetTime.toString(),
'Retry-After': Math.ceil((result.resetTime - Date.now()) / 1000).toString()
}
}
)
}
// Add rate limit headers to successful responses
const response = NextResponse.next()
response.headers.set('X-RateLimit-Remaining', result.remaining.toString())
response.headers.set('X-RateLimit-Reset', result.resetTime.toString())
return response
}
}
Security Headers
Security Headers Middleware
// lib/middleware/security-headers.ts
import { NextResponse } from 'next/server'
export function addSecurityHeaders(response: NextResponse) {
// Content Security Policy
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
"style-src 'self' 'unsafe-inline'; " +
"img-src 'self' data: https:; " +
"font-src 'self' data:; " +
"connect-src 'self' https: wss:; " +
"frame-ancestors 'none';"
)
// Other security headers
response.headers.set('X-Frame-Options', 'DENY')
response.headers.set('X-Content-Type-Options', 'nosniff')
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
response.headers.set('X-XSS-Protection', '1; mode=block')
response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
)
return response
}
Best Practices
1. Authentication Best Practices
- Use strong, unique JWT secrets
- Implement token rotation
- Set appropriate token expiration times
- Store tokens securely (httpOnly cookies for web)
2. Authorization Best Practices
- Implement least privilege principle
- Use role-based access control
- Validate permissions on every request
- Implement resource-level authorization
3. Input Validation Best Practices
- Validate all input data
- Use type-safe validation schemas
- Sanitize output data
- Implement size limits for requests
4. Rate Limiting Best Practices
- Implement different limits for different endpoints
- Use user-based limits for authenticated requests
- Provide clear error messages
- Include retry-after headers
5. Security Monitoring
- Log all authentication attempts
- Monitor for suspicious patterns
- Implement alerting for security events
- Regular security audits
This comprehensive API security implementation provides multiple layers of protection while maintaining good developer experience and performance.