The Challenge
A multi-tenant CRM with partner portals requires complex authorization: Authentication Layers:- OAuth2 token validation (who you are)
- OAuth2 scope checking (what you’re allowed to do)
- Step-up authentication (re-verify for sensitive operations)
- Partner context validation (which organization you represent)
- Capsule isolation (which tenant’s data you can access)
- Role-based permissions (admin vs sales rep)
- Record-level scopes (own records vs team vs territory vs all)
- Query-level filtering (enforce at database, not post-filter)
- OAuth scopes prevent unauthorized API access
- Step-up auth protects sensitive operations like financial transactions
- Partner context ensures multi-org isolation
- Data scopes implement CRM-style “who sees what records”
The Solution
We built five authorization layers, each enforced at the appropriate architectural boundary.Layer 1: OAuth2 Scope Validation
API endpoints require specific OAuth2 scopes:RequireScope middleware validates tokens before handlers execute:
Layer 2: Step-Up Authentication
Sensitive operations require recent authentication:Layer 3: Partner Context Validation
Multi-organization portals require partner context:Layer 4: Role-Based Data Scopes
CRM users need different data visibility based on their role:Layer 5: Query-Level Scope Filtering
Data scopes filter at the database query layer:Integration Example
Here’s how all layers work together:- OAuth scope validated by middleware
- Step-up auth checked if endpoint requires it
- Partner context validated if multi-org
- User role determines default data scope
- Database query filtered by scope
Implementation Scale
Partner Portal Security Middleware
Commit: b6e4780 Scope: 1,673 lines across 9 files Middleware Components:- RequireStepUp (250 lines)
- RequireScope (234 lines)
- RequirePartnerContext (197 lines)
- 9 integration tests
- All validation scenarios covered
- Step-up freshness verification
- Scope mismatch detection
- Partner context isolation
Owner-Based Access Control
Commit: e27a8d6 Scope: 1,143 lines across 7 files Domain Components:- CrmRecordScope enum with access evaluation (633 lines)
- ScopedAccessContext for query filtering
- CrmRole with default scopes
- Scope extractor logic (223 lines)
- list_with_scope method for scoped queries (214 lines)
- DynamoDB filter expression generation
- Handler integration (62 lines)
- 12 unit tests for scope filtering
- Authorization escalation tests
- Query-level filtering verification
Key Learnings
1. Defense-in-Depth Works
Multiple authorization layers catch different failure modes:- Stolen tokens caught by scope validation
- Session hijacking caught by step-up auth
- Cross-partner access caught by context validation
- Unauthorized data viewing caught by scope filtering
2. Enforce at Architectural Boundaries
Each layer enforces at the appropriate boundary:- Middleware: OAuth scopes, step-up auth, partner context
- Application layer: Role-to-scope derivation
- Query layer: Scope filtering
3. Make Authorization Declarative
OpenAPI security annotations make requirements explicit:4. Filter at the Database
Query-level filtering is more secure than post-filtering:5. AI Handles Systematic Patterns
The AI excelled at:- Middleware boilerplate - Extractors follow patterns
- Scope expression generation - Formulaic DynamoDB filters
- Test scenario coverage - Systematic authorization cases
- Integration wiring - Connecting layers consistently
- Security model design - Which layers to implement
- Scope semantics - What “Team” vs “Territory” means
- Escalation rules - When scope override is allowed
Results
Security Posture:- Defense-in-depth across 5 layers
- Declarative authorization requirements
- Query-level data filtering
- Comprehensive test coverage (21 tests)
- Complete implementation in 2 commits
- 2,816 lines of authorization infrastructure
- Estimated 6-8x faster than manual implementation
- Authorization centralized in middleware
- Business logic free of access checks
- OpenAPI documents security requirements
- Scope rules encoded in domain types
- Audit trail through middleware logging
- Explicit scope validation
- Partner data isolation
- Role-based access control
Conclusion
Multi-layer authorization isn’t over-engineering—it’s defense-in-depth: Layer 1 (OAuth): Validates who you are and what you can do at the API level. Layer 2 (Step-Up): Protects sensitive operations with freshness checks. Layer 3 (Partner Context): Enforces multi-organization isolation. Layer 4 (Roles): Determines default data visibility. Layer 5 (Scopes): Filters database queries by ownership. Each layer addresses different threat models. Removing any layer creates vulnerabilities. The key insights:- Enforce at architectural boundaries (middleware, query layer)
- Make requirements declarative (OpenAPI security)
- Filter at the database (not post-filtering)
- Test systematically (all authorization paths)