Security Architecture Overview
Security Architecture Overview
Section titled “Security Architecture Overview”This guide explains the comprehensive security architecture in banyan-core, including the critical two-layer authorization model that separates permission-based access control from business policy enforcement.
Use This Guide If…
Section titled “Use This Guide If…”- You’re designing security for a new microservice
- You need to understand when to use Layer 1 vs Layer 2 authorization
- You’re implementing authentication flows
- You want to understand the complete request security lifecycle
- You’re integrating external identity providers
Security Model Philosophy
Section titled “Security Model Philosophy”The banyan-core platform implements a defense-in-depth security model with multiple layers of protection:
- Layer 0: Transport Security - TLS/HTTPS encryption (infrastructure)
- Layer 1: Permission-Based Authorization - WHO can call WHAT (API Gateway)
- Layer 2: Policy-Based Authorization - WHEN and HOW operations can execute (Service Handlers)
- Layer 3: Data Security - Field-level encryption, audit logging (application)
This guide focuses on Layers 1 and 2 - the two-layer authorization model.
Two-Layer Authorization Model
Section titled “Two-Layer Authorization Model”Why Two Layers?
Section titled “Why Two Layers?”Traditional authorization often conflates two separate concerns:
- Access Control: Can this user call this operation at all?
- Business Rules: Should this operation execute given the current context?
Mixing these concerns leads to:
- Repeated authorization logic across services
- Tight coupling between services and auth systems
- Difficulty testing business rules independently
- Complex authorization code scattered throughout handlers
The two-layer model separates these concerns for cleaner architecture.
Layer 1: Permission-Based Authorization (API Gateway)
Section titled “Layer 1: Permission-Based Authorization (API Gateway)”Location: API Gateway (before message creation)
Purpose: Enforce WHO can call WHAT operations
Mechanism: @Command() and @Query() decorator permissions
Technology: JWT token validation + permission checking
// In contract definition@Command({ description: 'Create a new product', permissions: ['product:create'] // Layer 1 requirement})export class CreateProductCommand { name: string; price: number;}Layer 1 Flow:
- Client sends request with JWT token
- API Gateway validates JWT signature
- Gateway extracts permissions from token
- Gateway checks contract’s
permissionsarray - If user lacks required permission → 403 Forbidden (request rejected)
- If user has permission → Route to service via message bus
Characteristics:
- Happens at gateway (centralized)
- Fast permission checking
- Based on static permissions in JWT
- No business context awareness
- Prevents unauthorized requests from reaching services
Layer 2: Policy-Based Authorization (Service Handlers)
Section titled “Layer 2: Policy-Based Authorization (Service Handlers)”Location: Service handlers (during message processing)
Purpose: Enforce WHEN and HOW operations can execute
Mechanism: @RequirePolicy() decorator + business logic
Technology: Custom policy functions with business context
// In handler implementation@CommandHandlerDecorator(CreateProductCommand)export class CreateProductHandler extends CommandHandler<...> { @RequirePolicy(async (user, command) => { // Layer 2: Business rules if (!user) { throw new Error('Authentication required'); }
// Can only create products in categories you manage const userManagesCategory = await checkCategoryOwnership( user.userId, command.categoryId );
if (!userManagesCategory) { throw new PolicyViolationError( 'CreateProductHandler', user.userId, 'category_ownership', 'You can only create products in categories you manage' ); } }) async handle(command: CreateProductCommand, user: AuthenticatedUser | null) { // Business logic here - policy already enforced }}Layer 2 Flow:
- Message arrives from gateway (Layer 1 passed)
- BaseService extracts
@RequirePolicymetadata - Platform executes policy function with user + message
- If policy fails → Throw
PolicyViolationError(operation rejected) - If policy passes → Execute
handler.handle()
Characteristics:
- Happens in service (distributed)
- Context-aware business rules
- Access to full message payload
- Can query other services/databases
- Enforces temporal, ownership, state-based constraints
Comparison: Layer 1 vs Layer 2
Section titled “Comparison: Layer 1 vs Layer 2”| Aspect | Layer 1 (Permission) | Layer 2 (Policy) |
|---|---|---|
| Location | API Gateway | Service Handler |
| Timing | Before message creation | During message processing |
| Decorator | @Command() / @Query() | @RequirePolicy() |
| Checks | Static permissions | Dynamic business rules |
| Context | JWT claims only | Full message + database access |
| Speed | Very fast | May require queries |
| Purpose | Coarse-grained access control | Fine-grained business rules |
| Examples | ”user:create”, “order:read" | "Can only edit own orders”, “Must be draft status” |
Complete Request Security Flow
Section titled “Complete Request Security Flow”Let’s trace a complete request through both security layers:
┌─────────────────────────────────────────────────────────────────┐│ CLIENT REQUEST ││ POST /api/create-product ││ Authorization: Bearer eyJhbGc... ││ { "name": "Widget", "categoryId": "cat-123" } │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ LAYER 0: TRANSPORT SECURITY ││ ✓ TLS encryption verified ││ ✓ HTTPS connection established │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ LAYER 1: PERMISSION-BASED AUTHORIZATION (API Gateway) ││ ││ 1. Extract JWT from Authorization header ││ 2. Validate JWT signature (JWKS or shared secret) ││ 3. Check expiry (exp claim) ││ 4. Extract user permissions from token ││ → permissions: ["product:read", "product:create"] ││ ││ 5. Load contract for CreateProductCommand ││ → requiredPermissions: ["product:create"] ││ ││ 6. Check: Does user have "product:create"? ││ → YES ✓ Continue to service ││ → NO ✗ Return 403 Forbidden (request never reaches service)│└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ MESSAGE BUS ││ Publishes CreateProductCommand to product-service queue │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ LAYER 2: POLICY-BASED AUTHORIZATION (Service Handler) ││ ││ 1. BaseService receives message from queue ││ 2. BaseService extracts @RequirePolicy metadata ││ 3. Execute policy function: ││ async (user, command) => { ││ // Business rule: Must be authenticated ││ if (!user) throw PolicyAuthenticationError(); ││ ││ // Business rule: Category must exist ││ const category = await db.findCategory(command.categoryId);││ if (!category) throw PolicyViolationError(); ││ ││ // Business rule: Must manage this category ││ if (!category.managers.includes(user.userId)) { ││ throw PolicyViolationError('Not category manager'); ││ } ││ ││ // Business rule: Category must be active ││ if (category.status !== 'active') { ││ throw PolicyViolationError('Category inactive'); ││ } ││ } ││ ││ 4. Policy passed? Execute handler.handle() ││ Policy failed? Return error (operation rejected) │└─────────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────┐│ BUSINESS LOGIC EXECUTION ││ - Create Product aggregate ││ - Apply business rules ││ - Emit domain events ││ - Save to event store │└─────────────────────────────────────────────────────────────────┘When to Use Each Layer
Section titled “When to Use Each Layer”Use Layer 1 (Permission-Based) When:
Section titled “Use Layer 1 (Permission-Based) When:”✓ Enforcing coarse-grained access control ✓ Checking static permissions (create, read, update, delete) ✓ Need to reject requests early before they reach services ✓ Permission is based only on user identity, not context ✓ Want centralized permission management
Examples:
- “Can this user create products at all?”
- “Does this user have read access to orders?”
- “Is this user allowed to delete categories?”
Use Layer 2 (Policy-Based) When:
Section titled “Use Layer 2 (Policy-Based) When:”✓ Enforcing fine-grained business rules ✓ Checking contextual constraints (ownership, state, time) ✓ Need to query data to make authorization decision ✓ Rules depend on current system state ✓ Want domain-specific authorization logic
Examples:
- “Can this user edit THIS SPECIFIC order?” (ownership)
- “Can products be created in THIS category?” (state-based)
- “Is it currently allowed to process returns?” (temporal)
- “Does this user manage the category they’re creating in?” (relationship)
Security Best Practices
Section titled “Security Best Practices”1. Always Use Both Layers
Section titled “1. Always Use Both Layers”Every operation should have Layer 1 permissions, even if it’s just an empty array for public endpoints.
// GOOD: Explicit about permissions@Command({ description: 'Public health check', permissions: [] // Explicitly public})export class HealthCheckCommand {}
// BAD: Unclear if permissions were forgotten@Command({ description: 'Create product' // Missing permissions - is this a bug?})2. Layer 1 Protects Against Brute Force
Section titled “2. Layer 1 Protects Against Brute Force”Layer 1 prevents unauthorized users from flooding your services with invalid requests.
// Without Layer 1, attackers could send millions of invalid requests// to your service, causing:// - Database load from policy checks// - Service degradation// - Log spam
// With Layer 1, invalid requests are rejected at the gateway// before consuming service resources3. Layer 2 Enforces Business Integrity
Section titled “3. Layer 2 Enforces Business Integrity”Layer 1 alone is insufficient for complex business rules.
// Layer 1: User has "order:update" permission ✓// But Layer 2 enforces:// - Can only update own orders (ownership)// - Can only update pending orders (state)// - Can only update during business hours (temporal)// - Can only update if inventory available (consistency)4. Fail Securely
Section titled “4. Fail Securely”Both layers should fail closed (deny by default).
// GOOD: Explicit denial@RequirePolicy(async (user, command) => { if (!user) { throw new PolicyAuthenticationError(...); } // ... business rules})
// BAD: Silent failure@RequirePolicy(async (user, command) => { if (user) { // Only check if user exists - silently allows if no user }})5. Audit Both Layers
Section titled “5. Audit Both Layers”Log authorization decisions at both layers for security monitoring.
// Layer 1 logs in API GatewayLogger.info('Permission check', { user: user.userId, operation: 'CreateProduct', requiredPermissions: ['product:create'], userPermissions: user.permissions, result: 'allowed'});
// Layer 2 logs in serviceLogger.info('Policy check', { user: user.userId, handler: 'CreateProductHandler', policyName: 'CreateProductBusinessRules', result: 'allowed', context: { categoryId: command.categoryId }});Authentication vs Authorization
Section titled “Authentication vs Authorization”Authentication: WHO are you? Authorization: WHAT can you do?
The platform separates these concerns:
| Concern | Component | Responsibility |
|---|---|---|
| Authentication | Auth Service | Validate credentials, issue JWT tokens |
| Authorization Layer 1 | API Gateway | Check JWT permissions against contract requirements |
| Authorization Layer 2 | Service Handlers | Enforce business policies with full context |
See authentication.md for authentication details.
Common Security Mistakes
Section titled “Common Security Mistakes”❌ Mistake 1: Only Using Layer 1
Section titled “❌ Mistake 1: Only Using Layer 1”// User has "order:update" permission// But no checks for:// - Is this their order?// - Is the order in an editable state?// - Are they allowed to change this specific field?
// Result: Authorization bypass via permission escalation❌ Mistake 2: Only Using Layer 2
Section titled “❌ Mistake 2: Only Using Layer 2”// No Layer 1 permissions// Result: Attackers can flood service with requests// Even if Layer 2 rejects them, it still consumes resources❌ Mistake 3: Confusing Validation with Authorization
Section titled “❌ Mistake 3: Confusing Validation with Authorization”// VALIDATION: Is the data well-formed?if (!command.email.includes('@')) { throw new ValidationError('Invalid email format');}
// AUTHORIZATION: Is the user allowed to perform this action?if (!user.permissions.includes('user:create')) { throw new AuthorizationError('Insufficient permissions');}
// Don't mix these - they serve different purposes❌ Mistake 4: Hardcoding Permissions
Section titled “❌ Mistake 4: Hardcoding Permissions”// BAD: Hardcoded permission checkif (user.permissions.includes('admin')) { // allow}
// GOOD: Use Layer 1 contract permissions + Layer 2 policies@Command({ permissions: ['user:delete'] })// ... handler with @RequirePolicy for business rulesDevelopment vs Production Security
Section titled “Development vs Production Security”Development Mode
Section titled “Development Mode”// Enable development auth bypass (local testing only)DEVELOPMENT_AUTH_ENABLED=true
// Send requests with dev headers (no JWT needed)X-Dev-User-Id: test-user-123X-Dev-Permissions: product:create,product:read⚠️ WARNING: NEVER enable DEVELOPMENT_AUTH_ENABLED in production!
Production Mode
Section titled “Production Mode”// Production requires real JWT validationJWT_SECRET=<secure-random-secret># ORJWKS_URI=https://your-identity-provider.com/.well-known/jwks.jsonJWT_ISSUER=https://your-identity-provider.com/JWT_AUDIENCE=https://your-api.com
// Development mode automatically disabledDEVELOPMENT_AUTH_ENABLED=false # or omit entirelyProduction Security Checklist
Section titled “Production Security Checklist”Before deploying to production, verify:
-
DEVELOPMENT_AUTH_ENABLEDis NOT set (or explicitly false) -
JWT_SECRETis cryptographically random (32+ characters) - JWKS_URI uses HTTPS (never HTTP)
- All contracts have explicit permissions (even
[]for public) - Critical operations have Layer 2 policies
- Authorization failures are logged for monitoring
- Token expiry is configured appropriately (5-15 minutes)
- Refresh tokens are implemented for session management
- TLS/HTTPS is enforced for all endpoints
- Security headers are configured (HSTS, CSP, etc.)
Next Steps
Section titled “Next Steps”Now that you understand the two-layer authorization model:
- Authentication: Learn how to implement JWT authentication
- Layer 1: Implement permission-based authorization
- Layer 2: Add policy-based authorization
- RBAC: Configure role-based access control
- External Auth: Integrate external auth providers
Related Guides
Section titled “Related Guides”- Authentication - JWT tokens, validation, development mode
- Permission-Based Authorization - Layer 1 implementation
- Policy-Based Authorization - Layer 2 with @RequirePolicy
- RBAC - Role-based access control patterns
- External Auth Providers - Auth0, Okta, OIDC integration
- Writing Handlers - Handler implementation patterns
- Defining Contracts - Contract permissions