How CoSec Supports Multi-Tenancy: Architecture and Implementation Guide
CoSec implements multi-tenancy by embedding tenant identifiers into security principals, filtering policies by tenant ID, and propagating tenant context through tokens and the security context pipeline.
CoSec (Comprehensive Security) is an open-source security framework designed to handle complex authorization scenarios in SaaS environments. Understanding how CoSec supports multi-tenancy is essential for architects building applications that require strict data isolation between tenants while sharing a single deployment.
Core Tenant Abstraction
The Tenant Model
At the heart of CoSec’s multi-tenancy architecture lies the Tenant interface defined in cosec-api/src/main/kotlin/me/ahoo/cosec/api/tenant/Tenant.kt. This abstraction provides a unique tenantId and helper predicates such as isPlatformTenant(), isDefaultTenant(), and isUserTenant() to distinguish between system-level and customer-level tenants.
The concrete implementation resides in cosec-core/src/main/kotlin/me/ahoo/cosec/tenant/SimpleTenant.kt, which acts as a simple value object holding the tenant identifier. This design allows the framework to treat tenant identification as a first-class citizen throughout the security lifecycle.
Principal-Tenant Coupling
CoSec embeds tenant information directly into authentication principals via the TenantCapable interface located in cosec-api/src/main/kotlin/me/ahoo/cosec/api/tenant/TenantCapable.kt. By mixing this capability into users, services, and other security entities, the framework ensures that every authenticated subject carries its tenant affiliation. This coupling guarantees that tenant context is never lost as requests traverse the security pipeline.
Tenant Context Propagation
Security Context Integration
The SecurityContext interface in cosec-api/src/main/kotlin/me/ahoo/cosec/api/context/SecurityContext.kt serves as the central holder for both the current principal and its associated tenant. When a request enters the system, the security context captures the tenant via securityContext.tenant, making it readily available to all downstream authorization checks, policy evaluators, and audit loggers.
Token-Based Tenant Embedding
CoSec propagates tenant identity through JWT tokens using cosec-jwt/src/main/kotlin/me/ahoo/cosec/jwt/JwtTokenConverter.kt. During token generation, the framework embeds the tenant identifier as a claim using the key Tenant.TENANT_ID_KEY. When parsing tokens, the converter creates a SimpleTokenTenantPrincipal (defined in cosec-core/src/main/kotlin/me/ahoo/cosec/token/SimpleTokenTenantPrincipal.kt) that implements TenantCapable, ensuring the tenant survives stateless authentication.
Policy Isolation and Evaluation
Tenant-Scoped Policies
Every policy record in CoSec stores a tenantId field within cosec-core/src/main/kotlin/me/ahoo/cosec/policy/PolicyData.kt. This storage strategy ensures that authorization rules are physically or logically partitioned by tenant at the data layer.
Policy Evaluation Flow
During authorization, the policy evaluator first filters the policy repository by matching policy.tenantId against securityContext.tenant.tenantId. Only policies belonging to the requesting tenant are considered for rule evaluation, guaranteeing strict isolation between tenant authorization domains.
Advanced Multi-Tenancy Features
Dynamic Tenant Switching
For administrative scenarios requiring impersonation or tenant hopping, CoSec provides cosec-core/src/main/kotlin/me/ahoo/cosec/authentication/token/AbstractSwitchTenantAuthentication.kt. This abstraction allows an already-authenticated principal to change its tenant context on-the-fly without requiring re-authentication, enabling support desks and platform administrators to operate across tenant boundaries securely.
Serialization and Observability
Tenant persistence is handled by cosec-core/src/main/kotlin/me/ahoo/cosec/serialization/JsonPolicySerializer.kt, which automatically writes and reads the tenantId field during JSON serialization. For operational visibility, cosec-opentelemetry/src/main/kotlin/me/ahoo/cosec/opentelemetry/CoSecInstrumenter.kt attaches the tenant identifier as an attribute to OpenTelemetry spans and metrics, enabling per-tenant monitoring and audit trails.
Implementation Example
The following Kotlin example demonstrates the end-to-end tenant flow in CoSec:
// 1️⃣ Parse token → embed tenant
val jwt = Jwts.parserBuilder().build().parseClaimsJws(tokenString)
val tenantId = jwt.body.get(Tenant.TENANT_ID_KEY, String::class.java)
val tenant = SimpleTenant(tenantId)
val principal = SimpleTokenTenantPrincipal(tokenPrincipal, tenant)
// 2️⃣ Build security context
val securityContext = SimpleSecurityContext(principal)
// 3️⃣ Policy evaluation (tenant‑aware)
val applicablePolicies = policyRepository.findByTenantId(securityContext.tenant.tenantId)
val decision = policyEvaluator.evaluate(applicablePolicies, securityContext, request)
This pattern ensures that tenant context flows from the initial token parsing through to final authorization decisions.
Summary
- First-class Tenant abstraction: The
Tenantinterface inTenant.ktprovides clear semantics for platform, default, and user tenants. - Embedded tenant context:
TenantCapablemixes tenant information into every principal, ensuring the tenant identifier travels with the authentication subject. - Policy-level isolation:
PolicyData.ktstorestenantIdalongside rules, and the evaluator filters policies by tenant before execution. - Cross-layer propagation: Tenant identifiers flow through JWT claims, security contexts, JSON serialization, and OpenTelemetry instrumentation for complete observability.
Frequently Asked Questions
How is the tenant ID stored in JWT tokens?
CoSec stores the tenant identifier as a JWT claim using the constant Tenant.TENANT_ID_KEY. The JwtTokenConverter class extracts this claim during token parsing and constructs a SimpleTokenTenantPrincipal that carries the tenant forward into the security context.
Can a user switch tenants after authentication?
Yes. The AbstractSwitchTenantAuthentication class supports dynamic tenant switching for scenarios such as administrative impersonation or support desk operations. This allows authenticated principals to change their effective tenant context without re-authenticating.
How does CoSec ensure policy isolation between tenants?
CoSec ensures isolation by storing a tenantId field in every PolicyData record. During policy evaluation, the framework filters the policy repository to match only policies where policy.tenantId equals the current securityContext.tenant.tenantId, preventing cross-tenant authorization leakage.
Is tenant information available in logs and metrics?
Yes. The CoSecInstrumenter class automatically attaches the tenant identifier to OpenTelemetry spans and metrics as an attribute. This enables per-tenant observability in distributed tracing systems and monitoring dashboards.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →