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.