API Security Guide
API Security Guide
Section titled “API Security Guide”Overview
Section titled “Overview”The API Gateway provides two-layer security for all external APIs:
- Layer 1: Permission-based at API Gateway - Who can call what
- Layer 2: Policy-based at service handlers - Business rule enforcement
Authentication
Section titled “Authentication”JWT Bearer Tokens
Section titled “JWT Bearer Tokens”All API requests (except public endpoints) require JWT authentication:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ http://localhost:3003/api/usersJWT Token Structure
Section titled “JWT Token Structure”{ "sub": "usr_1234567890", "email": "alice@example.com", "name": "Alice Smith", "permissions": ["users:read", "users:create", "orders:read"], "iat": 1700000000, "exp": 1700003600}Required Claims:
sub- User IDemail- User emailname- User display namepermissions- Array of permission stringsexp- Token expiration (Unix timestamp)
Obtaining JWT Tokens
Section titled “Obtaining JWT Tokens”Login Endpoint
Section titled “Login Endpoint”POST http://localhost:3003/api/auth/loginContent-Type: application/json
{ "email": "alice@example.com", "password": "SecurePassword123"}Response:
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expiresIn": 3600, "user": { "id": "usr_1234567890", "email": "alice@example.com", "name": "Alice Smith" }}External Auth (Auth0)
Section titled “External Auth (Auth0)”The platform supports Auth0 integration:
// Environment configurationAUTH0_DOMAIN=your-tenant.us.auth0.comAUTH0_AUDIENCE=https://api.your-service.comAUTH0_ISSUER=https://your-tenant.us.auth0.com/Use Auth0 SDK to obtain tokens, then pass to API Gateway.
Authorization
Section titled “Authorization”Permission-Based (Layer 1)
Section titled “Permission-Based (Layer 1)”Enforced at the API Gateway before messages reach services.
Defining Required Permissions
Section titled “Defining Required Permissions”export const CreateUserContract = createContract({ messageType: 'CreateUserCommand', targetService: 'user-service', requiredPermissions: ['users:create'], // ← Required permission // ...});Permission Check Flow
Section titled “Permission Check Flow”- Client sends request with JWT token
- API Gateway validates JWT signature
- Gateway extracts
permissionsclaim - Gateway checks if user has ALL required permissions
- If yes → route to service, if no → 403 Forbidden
Permission Naming
Section titled “Permission Naming”Use resource:action pattern:
users:read - Read user datausers:create - Create new usersusers:update - Update existing usersusers:delete - Delete usersusers:admin - Full user management
orders:read - View ordersorders:process - Process ordersorders:cancel - Cancel orders
reports:generate - Generate reportsreports:export - Export report dataPolicy-Based (Layer 2)
Section titled “Policy-Based (Layer 2)”Enforced in service handlers for business rules.
Example: Resource Ownership
Section titled “Example: Resource Ownership”@QueryHandler(GetUserContract)export class GetUserHandler { async handle(input: { id: string }, context: MessageContext) { const requestingUserId = context.userId; const targetUserId = input.id;
// Policy: Users can only read their own data (unless admin) if (requestingUserId !== targetUserId && !context.permissions.includes('users:admin')) { throw new UnauthorizedError( 'You can only access your own user data' ); }
return await this.userRepository.findById(targetUserId); }}Example: Time-Based Access
Section titled “Example: Time-Based Access”@CommandHandler(ProcessOrderContract)export class ProcessOrderHandler { async handle(input: { orderId: string }, context: MessageContext) { const order = await this.orderRepository.findById(input.orderId);
// Policy: Orders can only be processed during business hours const now = new Date(); const hour = now.getHours(); if (hour < 9 || hour >= 17) { throw new BusinessRuleError( 'Orders can only be processed between 9 AM and 5 PM' ); }
// Process order... }}Rate Limiting
Section titled “Rate Limiting”Default Limits
Section titled “Default Limits”The API Gateway enforces per-user rate limits:
| User Type | Limit | Window |
|---|---|---|
| Authenticated | 100 requests | 1 minute |
| Anonymous | 10 requests | 1 minute |
Rate Limit Headers
Section titled “Rate Limit Headers”Responses include rate limit information:
X-RateLimit-Limit: 100X-RateLimit-Remaining: 95X-RateLimit-Reset: 1700000060Rate Limit Exceeded
Section titled “Rate Limit Exceeded”When limit is exceeded, API returns:
HTTP/1.1 429 Too Many RequestsRetry-After: 30
{ "error": "TooManyRequests", "message": "Rate limit exceeded. Try again in 30 seconds.", "correlationId": "cor_abc123xyz", "retryAfter": 30}Custom Rate Limits
Section titled “Custom Rate Limits”Configure per-contract limits:
export const BulkImportContract = createContract({ messageType: 'BulkImportCommand', targetService: 'import-service', metadata: { rateLimit: { requests: 10, window: 3600 // 10 requests per hour } }, // ...});Public Endpoints
Section titled “Public Endpoints”Defining Public Contracts
Section titled “Defining Public Contracts”Endpoints accessible without authentication:
export const RegisterUserContract = createContract({ messageType: 'RegisterUserCommand', targetService: 'auth-service', isPublic: true, // ← No authentication required requiredPermissions: [], // Must be empty for public inputSchema: { type: 'object', properties: { email: { type: 'string', format: 'email' }, password: { type: 'string', minLength: 8 }, name: { type: 'string' } }, required: ['email', 'password', 'name'] }, // ...});Public Endpoint Examples
Section titled “Public Endpoint Examples”# User registration (public)POST http://localhost:3003/api/auth/register
# Login (public)POST http://localhost:3003/api/auth/login
# Password reset (public)POST http://localhost:3003/api/auth/reset-password
# Public data query (public)GET http://localhost:3003/api/productsCORS Configuration
Section titled “CORS Configuration”Default CORS Policy
Section titled “Default CORS Policy”The API Gateway allows cross-origin requests:
{ origin: '*', // Allow all origins (configure for production) methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 86400 // 24 hours}Preflight Requests
Section titled “Preflight Requests”Browser automatically sends OPTIONS request:
OPTIONS http://localhost:3003/api/usersOrigin: http://localhost:3000Access-Control-Request-Method: POSTAccess-Control-Request-Headers: Authorization, Content-TypeResponse:
HTTP/1.1 204 No ContentAccess-Control-Allow-Origin: http://localhost:3000Access-Control-Allow-Methods: POST, GET, PUT, DELETEAccess-Control-Allow-Headers: Authorization, Content-TypeAccess-Control-Max-Age: 86400Security Best Practices
Section titled “Security Best Practices”1. Use Strong Permissions
Section titled “1. Use Strong Permissions”// Good: Granular permissionsrequiredPermissions: ['users:create']requiredPermissions: ['orders:process']
// Avoid: Overly broadrequiredPermissions: ['admin']requiredPermissions: []2. Validate Input Thoroughly
Section titled “2. Validate Input Thoroughly”inputSchema: { type: 'object', properties: { email: { type: 'string', format: 'email', maxLength: 255 // Prevent abuse }, password: { type: 'string', minLength: 8, maxLength: 100, pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).*$' } }, required: ['email', 'password']}3. Implement Business Policies
Section titled “3. Implement Business Policies”@CommandHandler(DeleteUserContract)export class DeleteUserHandler { async handle(input: { id: string }, context: MessageContext) { // Policy: Cannot delete yourself if (input.id === context.userId) { throw new BusinessRuleError('Cannot delete your own account'); }
// Policy: Only admins can delete users if (!context.permissions.includes('users:admin')) { throw new UnauthorizedError('Admin permission required'); }
await this.userRepository.delete(input.id); }}4. Protect Sensitive Data
Section titled “4. Protect Sensitive Data”outputSchema: { type: 'object', properties: { id: { type: 'string' }, email: { type: 'string' }, name: { type: 'string' }, // DON'T include password hash, tokens, etc. }}5. Use Short Token Expiration
Section titled “5. Use Short Token Expiration”// Environment configurationJWT_EXPIRATION=300 // 5 minutes (adjust as needed)Implement token refresh for better UX:
POST http://localhost:3003/api/auth/refreshError Responses
Section titled “Error Responses”401 Unauthorized
Section titled “401 Unauthorized”Missing or invalid authentication:
{ "error": "Unauthorized", "message": "Missing or invalid authentication token", "correlationId": "cor_abc123xyz", "timestamp": "2025-11-15T10:30:00Z"}403 Forbidden
Section titled “403 Forbidden”Insufficient permissions:
{ "error": "Forbidden", "message": "Missing required permission: users:create", "correlationId": "cor_abc123xyz", "timestamp": "2025-11-15T10:30:00Z", "requiredPermissions": ["users:create"], "userPermissions": ["users:read"]}429 Too Many Requests
Section titled “429 Too Many Requests”Rate limit exceeded:
{ "error": "TooManyRequests", "message": "Rate limit exceeded. Try again in 30 seconds.", "correlationId": "cor_abc123xyz", "retryAfter": 30, "limit": 100, "window": 60}Message Context
Section titled “Message Context”Handlers receive authentication context automatically:
interface MessageContext { userId: string; // Authenticated user ID email: string; // User email name: string; // User name permissions: string[]; // User permissions correlationId: string; // Request correlation ID timestamp: Date; // Request timestamp}Usage in handlers:
@CommandHandler(CreateOrderContract)export class CreateOrderHandler { async handle(input: { productId: string }, context: MessageContext) { // Access authenticated user const userId = context.userId; const userEmail = context.email;
// Check permissions programmatically if (!context.permissions.includes('orders:create')) { throw new UnauthorizedError('Permission denied'); }
// Create order for authenticated user return await this.orderRepository.create({ userId, productId: input.productId, createdBy: userEmail }); }}Development Mode
Section titled “Development Mode”Bypass Authentication (Development Only)
Section titled “Bypass Authentication (Development Only)”// Environment configurationDEVELOPMENT_AUTH_ENABLED=trueWARNING: Never enable in production!
When enabled, requests without auth are allowed with default permissions.
Troubleshooting
Section titled “Troubleshooting””Missing or invalid authentication token”
Section titled “”Missing or invalid authentication token””Cause: No Authorization header or invalid JWT
Solution:
curl -H "Authorization: Bearer YOUR_VALID_JWT" \ http://localhost:3003/api/users“Missing required permission: users:create”
Section titled ““Missing required permission: users:create””Cause: User JWT lacks required permission
Solution: Ensure user has permission granted in auth system
”Rate limit exceeded”
Section titled “”Rate limit exceeded””Cause: Too many requests in time window
Solution: Wait for retryAfter seconds or reduce request rate
”CORS policy blocked”
Section titled “”CORS policy blocked””Cause: Browser blocking cross-origin request
Solution: Configure CORS in API Gateway or use proxy in development
Next Steps
Section titled “Next Steps”- API Contracts - Defining secure contracts
- REST API Guide - Building RESTful endpoints
- GraphQL API Guide - Creating GraphQL schemas