Permission-Based Authorization (Layer 1)
Permission-Based Authorization (Layer 1)
Section titled “Permission-Based Authorization (Layer 1)”This guide covers Layer 1 authorization - permission-based access control enforced at the API Gateway BEFORE requests reach your services.
Use This Guide If…
Section titled “Use This Guide If…”- You’re defining permissions for commands and queries
- You want to enforce WHO can call WHAT operations
- You need to protect services from unauthorized requests
- You’re implementing coarse-grained access control
- You want to understand the
permissionsfield in contracts
Layer 1 Authorization Overview
Section titled “Layer 1 Authorization Overview”Layer 1 is the first line of defense in the two-layer authorization model:
- Location: API Gateway (centralized)
- Timing: BEFORE message creation and routing
- Mechanism: Contract
permissionsarray - Purpose: Enforce static, coarse-grained access control
- Question: “Does this user have permission to call this operation AT ALL?”
How It Works
Section titled “How It Works”┌─────────┐│ Request │ Authorization: Bearer <JWT>└────┬────┘ │ ▼┌────────────────────────────────────────────────┐│ API Gateway - Layer 1 ││ ││ 1. Validate JWT signature and expiry ││ 2. Extract permissions from JWT payload ││ → ["product:read", "product:create"] ││ ││ 3. Load contract for operation ││ → @Command({ permissions: ["product:create"] })│ ││ 4. Check: Does user have ALL required perms? ││ → Required: ["product:create"] ││ → User has: ["product:read", "product:create"]│ → ✓ Match found ││ ││ 5. Permission check result: ││ → PASS: Route to service via message bus ││ → FAIL: Return 403 Forbidden (stop here) │└────────────────────────────────────────────────┘If Layer 1 fails, the request never reaches the service - protecting your business logic from unauthorized access attempts.
Defining Permissions in Contracts
Section titled “Defining Permissions in Contracts”Permissions are declared in @Command() and @Query() decorators.
Command with Permissions
Section titled “Command with Permissions”import { Command } from '@banyanai/platform-contract-system';
@Command({ description: 'Create a new product', permissions: ['product:create'] // Layer 1 requirement})export class CreateProductCommand { name: string; price: number; categoryId: string; description: string;}
export interface CreateProductResult { productId: string; name: string; price: number;}Query with Permissions
Section titled “Query with Permissions”import { Query } from '@banyanai/platform-contract-system';
@Query({ description: 'Get a product by ID', permissions: ['product:read'] // Layer 1 requirement})export class GetProductQuery { productId: string;}
export interface GetProductResult { productId: string; name: string; price: number; category: string;}Multiple Permissions (ALL required)
Section titled “Multiple Permissions (ALL required)”@Command({ description: 'Transfer product between warehouses', permissions: ['product:update', 'warehouse:manage'] // User must have BOTH})export class TransferProductCommand { productId: string; fromWarehouseId: string; toWarehouseId: string; quantity: number;}The gateway checks that the user has ALL permissions in the array.
Public Operations (No Permissions)
Section titled “Public Operations (No Permissions)”@Query({ description: 'Public health check', permissions: [] // Explicitly public - no auth required})export class HealthCheckQuery {}⚠️ Best Practice: Always explicitly set permissions: [] for public endpoints. This documents intent and prevents accidental exposure.
Permission Naming Conventions
Section titled “Permission Naming Conventions”Use a consistent naming pattern for permissions:
Standard Format: resource:action
Section titled “Standard Format: resource:action”// Resource-based permissions'product:create' // Create products'product:read' // Read products'product:update' // Update products'product:delete' // Delete products
'order:create' // Create orders'order:read' // Read orders'order:cancel' // Cancel orders'order:refund' // Refund orders
'user:create' // Create users'user:read' // Read users'user:update' // Update users'user:delete' // Delete usersHierarchical Permissions (Optional)
Section titled “Hierarchical Permissions (Optional)”// Service-level permissions'product-service:admin' // Full access to product service'order-service:admin' // Full access to order service
// Department-level permissions'sales:manager' // Sales department manager'warehouse:operator' // Warehouse operator
// Feature-level permissions'reports:export' // Export reports'analytics:view' // View analytics dashboardWildcard Permissions (Implementation Dependent)
Section titled “Wildcard Permissions (Implementation Dependent)”Some systems support wildcards for permission checking:
// User has: ['product:*']// Grants: 'product:create', 'product:read', 'product:update', 'product:delete'
// User has: ['*:read']// Grants: 'product:read', 'order:read', 'user:read', etc.⚠️ Note: Wildcard support depends on your permission checking implementation. The default gateway does exact string matching.
JWT Permissions
Section titled “JWT Permissions”Permissions are stored in the JWT token payload and extracted by the API Gateway.
Token Payload with Permissions
Section titled “Token Payload with Permissions”{ "sub": "user-123", "email": "alice@example.com", "permissions": [ "product:create", "product:read", "product:update", "order:read", "order:create" ], "iat": 1705334400, "exp": 1705334700}How Permissions Get Into Tokens
Section titled “How Permissions Get Into Tokens”Permissions are added to tokens during authentication:
async handle(command: AuthenticateUserCommand) { // 1. Validate credentials const user = await this.validateCredentials(command.email, command.password);
// 2. Query user's permissions from database const permissions = await this.getUserPermissions(user); // Returns: ['product:create', 'product:read', ...]
// 3. Query user's roles const roles = await this.getUserRoles(user);
// 4. Generate JWT with permissions const tokens = await this.jwtManager.generateTokenPair({ userId: user.id, email: user.email, permissions, // Include in token payload roles, });
return { success: true, accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, };}Permission Sources
Section titled “Permission Sources”Permissions can come from multiple sources (combined):
- Direct User Permissions: Assigned directly to the user
- Role Permissions: Inherited from user’s roles
- Group Permissions: Inherited from group membership (if implemented)
async getUserPermissions(user: UserReadModel): Promise<string[]> { // Direct permissions const directPermissions = user.directPermissions || [];
// Role-based permissions const rolePermissions: string[] = []; for (const roleId of user.roles || []) { const role = await RoleReadModel.findById(roleId); if (role?.permissions) { rolePermissions.push(...role.permissions); } }
// Combine and deduplicate return Array.from(new Set([...directPermissions, ...rolePermissions]));}Gateway Permission Checking
Section titled “Gateway Permission Checking”The API Gateway performs permission checks before routing requests.
Check Implementation
Section titled “Check Implementation”checkRequirePermissionDecorator( contract: Contract, userPermissions: string[]): AuthorizationResult { const requiredPermissions = contract.requiredPermissions;
// No permissions required - allow if (!requiredPermissions || requiredPermissions.length === 0) { return { authorized: true, requiredPermissions: [], userPermissions, }; }
// Check if user has ALL required permissions const missingPermissions = requiredPermissions.filter( (required) => !userPermissions.includes(required) );
if (missingPermissions.length === 0) { // User has all required permissions return { authorized: true, requiredPermissions: [...requiredPermissions], userPermissions, }; }
// User lacks required permissions return { authorized: false, error: { type: 'insufficient_permissions', message: `Missing required permissions: ${missingPermissions.join(', ')}`, requiredPermissions: [...requiredPermissions], userPermissions, }, requiredPermissions: [...requiredPermissions], userPermissions, };}Error Response
Section titled “Error Response”When permission check fails, the gateway returns:
{ "error": { "type": "insufficient_permissions", "message": "Missing required permissions: product:create", "requiredPermissions": ["product:create"], "userPermissions": ["product:read"] }}HTTP Status: 403 Forbidden
Common Permission Patterns
Section titled “Common Permission Patterns”CRUD Permissions
Section titled “CRUD Permissions”Standard create/read/update/delete permissions:
// Create@Command({ permissions: ['product:create'] })export class CreateProductCommand { ... }
// Read@Query({ permissions: ['product:read'] })export class GetProductQuery { ... }
// Update@Command({ permissions: ['product:update'] })export class UpdateProductCommand { ... }
// Delete@Command({ permissions: ['product:delete'] })export class DeleteProductCommand { ... }Admin Override
Section titled “Admin Override”Admin users with special permissions that bypass normal rules:
@Command({ description: 'Delete any user (admin only)', permissions: ['user:admin'] // Higher privilege than 'user:delete'})export class DeleteUserCommand { ... }Read-Only Access
Section titled “Read-Only Access”Users with read-only permissions:
// All queries require only 'read' permission@Query({ permissions: ['product:read'] })export class GetProductQuery { ... }
@Query({ permissions: ['product:read'] })export class ListProductsQuery { ... }
@Query({ permissions: ['product:read'] })export class SearchProductsQuery { ... }Tiered Permissions
Section titled “Tiered Permissions”Different permission levels for different operations:
// Basic tier - view only@Query({ permissions: ['reports:view'] })export class ViewReportQuery { ... }
// Standard tier - view and download@Query({ permissions: ['reports:download'] })export class DownloadReportQuery { ... }
// Premium tier - view, download, and schedule@Command({ permissions: ['reports:schedule'] })export class ScheduleReportCommand { ... }Testing Permission-Based Authorization
Section titled “Testing Permission-Based Authorization”Unit Testing Contract Permissions
Section titled “Unit Testing Contract Permissions”import { describe, expect, test } from '@jest/globals';import { CreateProductCommand } from '../contracts/commands.js';import { getContractMetadata } from '@banyanai/platform-contract-system';
describe('CreateProductCommand Permissions', () => { test('should require product:create permission', () => { const metadata = getContractMetadata(CreateProductCommand);
expect(metadata.requiredPermissions).toContain('product:create'); });
test('should not allow public access', () => { const metadata = getContractMetadata(CreateProductCommand);
expect(metadata.requiredPermissions.length).toBeGreaterThan(0); });});Integration Testing Gateway Checks
Section titled “Integration Testing Gateway Checks”describe('Gateway Permission Checks', () => { test('should reject request with missing permissions', async () => { // User has only 'product:read' const user: AuthenticatedUser = { userId: 'user-123', permissions: ['product:read'], roles: ['user'], };
const contract = { name: 'CreateProduct', requiredPermissions: ['product:create'], };
const result = jwtEngine.checkRequirePermissionDecorator( contract, user.permissions );
expect(result.authorized).toBe(false); expect(result.error?.type).toBe('insufficient_permissions'); });
test('should allow request with sufficient permissions', async () => { // User has required permission const user: AuthenticatedUser = { userId: 'user-123', permissions: ['product:read', 'product:create'], roles: ['user'], };
const contract = { name: 'CreateProduct', requiredPermissions: ['product:create'], };
const result = jwtEngine.checkRequirePermissionDecorator( contract, user.permissions );
expect(result.authorized).toBe(true); });});Development Testing with Headers
Section titled “Development Testing with Headers”# Test with sufficient permissionscurl -X POST http://localhost:3000/api/create-product \ -H "X-Dev-User-Id: alice" \ -H "X-Dev-Permissions: product:create" \ -H "Content-Type: application/json" \ -d '{"name":"Widget","price":29.99}'
# Expected: 200 OK
# Test with insufficient permissionscurl -X POST http://localhost:3000/api/create-product \ -H "X-Dev-User-Id: bob" \ -H "X-Dev-Permissions: product:read" \ -H "Content-Type: application/json" \ -d '{"name":"Widget","price":29.99}'
# Expected: 403 Forbidden# {# "error": {# "type": "insufficient_permissions",# "message": "Missing required permissions: product:create"# }# }Security Best Practices
Section titled “Security Best Practices”1. Always Specify Permissions Explicitly
Section titled “1. Always Specify Permissions Explicitly”// ✓ GOOD: Explicit permissions (even if empty)@Command({ description: 'Public health check', permissions: []})
// ✗ BAD: Unclear intent@Command({ description: 'Create user' // Missing permissions - bug or intentional?})2. Use Principle of Least Privilege
Section titled “2. Use Principle of Least Privilege”// ✓ GOOD: Minimal permission for operation@Query({ permissions: ['product:read'] // Only need read access})export class GetProductQuery { ... }
// ✗ BAD: Excessive permission requirement@Query({ permissions: ['product:admin'] // Why require admin for read?})export class GetProductQuery { ... }3. Separate Admin from Normal Operations
Section titled “3. Separate Admin from Normal Operations”// Normal operations@Command({ permissions: ['product:create'] })export class CreateProductCommand { ... }
// Administrative operations@Command({ permissions: ['product:admin'] })export class PurgeProductDataCommand { ... }4. Document Permission Requirements
Section titled “4. Document Permission Requirements”/** * Creates a new product in the catalog. * * Required permissions: * - product:create - Allows creating new products * * Note: Users also need 'category:read' to validate category existence * (enforced at Layer 2 in handler, not Layer 1) */@Command({ description: 'Create a new product', permissions: ['product:create']})export class CreateProductCommand { ... }5. Audit Permission Checks
Section titled “5. Audit Permission Checks”// Gateway logs all permission checksLogger.info('Permission check', { user: user.userId, operation: contract.name, requiredPermissions: contract.requiredPermissions, userPermissions: user.permissions, result: authorized ? 'allowed' : 'denied', missingPermissions: authorized ? [] : missingPermissions,});Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”❌ Mistake 1: Confusing Permissions with Policies
Section titled “❌ Mistake 1: Confusing Permissions with Policies”// BAD: Trying to encode business rules in permissionspermissions: ['product:create:in-category-123'] // Too specific
// GOOD: Use Layer 1 for access, Layer 2 for business rulespermissions: ['product:create'] // Layer 1// Then check category ownership in @RequirePolicy (Layer 2)❌ Mistake 2: Relying Only on Layer 1
Section titled “❌ Mistake 2: Relying Only on Layer 1”// User has 'order:update' permission// But Layer 1 alone doesn't check:// - Is this the user's order?// - Is the order in an editable state?// - Is the update allowed at this time?
// Need Layer 2 (@RequirePolicy) for these business rules❌ Mistake 3: Too Many Permissions Required
Section titled “❌ Mistake 3: Too Many Permissions Required”// BAD: Requiring multiple permissions when one suffices@Command({ permissions: ['product:create', 'product:read', 'category:read']})export class CreateProductCommand { ... }
// GOOD: Only require what's needed at Layer 1@Command({ permissions: ['product:create'] // Layer 1})// Check category access in handler (Layer 2)❌ Mistake 4: Hardcoding Permission Checks
Section titled “❌ Mistake 4: Hardcoding Permission Checks”// BAD: Hardcoding in handler (bypasses Layer 1)async handle(command: CreateProductCommand, user: AuthenticatedUser) { if (!user.permissions.includes('product:create')) { throw new Error('Insufficient permissions'); } // ...}
// GOOD: Declare in contract (Layer 1 handles it)@Command({ permissions: ['product:create'] })export class CreateProductCommand { ... }Troubleshooting
Section titled “Troubleshooting”Problem: “Missing required permissions” but user should have access
Section titled “Problem: “Missing required permissions” but user should have access”Debugging steps:
- Check JWT payload:
# Decode JWT to see permissionsecho "eyJhbGc..." | base64 -d | jq .- Verify permission name exact match:
// These are different:'product:create' // What user has'products:create' // What contract requires (typo!)- Check permission query:
// Ensure getUserPermissions() returns correct permissionsconst permissions = await getUserPermissions(user);console.log('User permissions:', permissions);Problem: Public endpoint requires authentication
Section titled “Problem: Public endpoint requires authentication”Check contract definition:
// Ensure permissions is explicitly empty array@Query({ permissions: [] // Must be explicit})Problem: Token has permissions but gateway still rejects
Section titled “Problem: Token has permissions but gateway still rejects”Check token validation:
// Is token being validated correctly?// Check gateway logs:grep "JWT validation" gateway.log
// Verify permissions extraction:grep "extractPermissionsFromClaims" gateway.logLayer 1 vs Layer 2 Decision Tree
Section titled “Layer 1 vs Layer 2 Decision Tree”Need to enforce a security rule?│├─ Is it about WHO can call the operation?│ └─ Use Layer 1 (Permission-based)│ Example: Only users with 'product:create' can create products│└─ Is it about WHEN/HOW the operation executes? └─ Use Layer 2 (Policy-based) Example: Users can only create products in categories they manageNext Steps
Section titled “Next Steps”Now that you understand Layer 1 permission-based authorization:
- Layer 2: Learn policy-based authorization for business rules
- RBAC: Implement role-based access control to manage permissions
- External Auth: Integrate external providers for SSO
- Testing: Read testing services for security testing
Related Guides
Section titled “Related Guides”- Security Overview - Two-layer authorization model
- Authentication - JWT tokens and validation
- Policy-Based Authorization - Layer 2 business rules
- RBAC - Role-based permission management
- Defining Contracts - Contract system details