Multi-Tenancy¶
Multi-tenancy is a core feature of USSO that allows you to serve multiple organizations (tenants) from a single USSO instance, with complete data isolation and per-tenant configuration.
What is Multi-Tenancy?¶
Think of USSO as an apartment building: - The building is your USSO instance - Each apartment is a tenant (organization) - Residents in one apartment can't access another apartment - Each apartment can have different decorations (configuration)
Why Multi-Tenancy?¶
For SaaS Applications: - Serve multiple companies from one deployment - Reduce infrastructure costs - Easier maintenance and updates - Faster onboarding of new customers
For Enterprise: - Separate departments or business units - Isolate test/staging environments - Manage multiple brands
Tenant Isolation¶
USSO provides complete isolation between tenants:
1. Data Isolation¶
Every record includes a tenant_id:
{
"id": "user:abc123",
"tenant_id": "org_company_a", # ← Isolates this user
"email": "[email protected]",
"roles": ["editor"]
}
All queries automatically filter by tenant:
# Fetch users - automatically scoped to tenant
users = await User.find(
User.tenant_id == current_tenant.id
).to_list()
2. Configuration Isolation¶
Each tenant has its own settings:
{
"id": "org_company_a",
"name": "Company A",
"domains": ["company-a.com", "app.company-a.com"],
"config": {
"require_mfa": true,
"session_timeout": 3600,
"allowed_login_methods": ["password", "oauth"]
},
"branding": {
"logo_url": "https://...",
"primary_color": "#FF6B6B"
}
}
3. Key Isolation¶
Each tenant has its own JWT signing keys:
{
"tenant_id": "org_company_a",
"keys": [
{
"id": "key_abc123",
"algorithm": "EdDSA",
"public_key": "...",
"private_key": "...", # Encrypted
"is_active": true
}
]
}
Benefits: - Tokens from one tenant can't access another - Per-tenant key rotation - Different security policies per tenant
Tenant Identification¶
USSO identifies tenants in two ways:
Option 1: Domain-Based (Recommended)¶
The tenant is determined by the domain in the request:
# Request to company-a.com
curl https://company-a.com/api/sso/v1/me
# → Resolves to org_company_a
# Request to company-b.com
curl https://company-b.com/api/sso/v1/me
# → Resolves to org_company_b
Setup:
- Register domains with tenant:
curl -X POST http://localhost:8000/api/sso/v1/tenants \
-H "Content-Type: application/json" \
-d '{
"name": "Company A",
"domains": ["company-a.com", "app.company-a.com"]
}'
- Configure DNS to point to your USSO instance
Option 2: Header-Based¶
Pass tenant ID in a header:
curl -X GET http://localhost:8000/api/sso/v1/me \
-H "X-Tenant-ID: org_company_a" \
-H "Authorization: Bearer YOUR_TOKEN"
Use cases: - Single domain for all tenants - Testing and development - Mobile apps
Creating Tenants¶
Via API¶
import requests
response = requests.post(
"http://localhost:8000/api/sso/v1/tenants",
headers={
"Authorization": f"Bearer {admin_token}",
"Content-Type": "application/json"
},
json={
"name": "My Company",
"slug": "my-company",
"domains": ["mycompany.com"],
"config": {
"require_mfa": False,
"session_timeout": 7200
}
}
)
tenant = response.json()
print(f"Tenant ID: {tenant['id']}")
const response = await fetch('http://localhost:8000/api/sso/v1/tenants', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'My Company',
slug: 'my-company',
domains: ['mycompany.com'],
config: {
require_mfa: false,
session_timeout: 7200
}
})
});
const tenant = await response.json();
console.log('Tenant ID:', tenant.id);
Default Tenant¶
On first startup, USSO creates a default tenant:
{
"id": "org_default",
"name": "Default Tenant",
"domains": ["localhost", "127.0.0.1", "dev.usso.io"],
"is_active": true
}
Tenant Configuration¶
Authentication Settings¶
{
"config": {
# Login methods
"allowed_login_methods": [
"password",
"magic_link",
"oauth",
"passkey"
],
# Security
"require_mfa": false,
"password_min_length": 8,
"password_require_special": true,
# Sessions
"session_timeout": 3600, # 1 hour
"refresh_token_ttl": 2592000, # 30 days
# Rate limiting
"login_attempts_limit": 5,
"login_attempts_window": 300 # 5 minutes
}
}
Messaging Configuration¶
Each tenant can have its own email/SMS providers:
{
"messaging": {
"email": {
"provider": "smtp",
"smtp_host": "smtp.company-a.com",
"smtp_port": 587,
"smtp_username": "[email protected]",
"from_address": "Company A <[email protected]>"
},
"sms": {
"provider": "twilio",
"account_sid": "...",
"auth_token": "..."
}
}
}
Branding¶
Customize the look and feel:
{
"branding": {
"logo_url": "https://cdn.company-a.com/logo.png",
"primary_color": "#FF6B6B",
"secondary_color": "#4ECDC4",
"company_name": "Company A Inc.",
"support_email": "[email protected]"
}
}
Workspaces Within Tenants¶
Tenants can have multiple workspaces for further organization:
Tenant: Company A (org_company_a)
├── Workspace: Engineering (ws_engineering)
│ ├── User: [email protected] (Developer)
│ └── User: [email protected] (Team Lead)
├── Workspace: Marketing (ws_marketing)
│ ├── User: [email protected] (Manager)
│ └── User: [email protected] (Designer)
└── Workspace: Sales (ws_sales)
└── User: [email protected] (Sales Rep)
Benefits: - Further data segmentation within a tenant - Different permissions per workspace - Users can belong to multiple workspaces
Access Patterns¶
Pattern 1: Shared SaaS¶
One USSO instance serving multiple companies:
USSO Instance
├── Tenant: Company A
│ ├── 1,000 users
│ └── 5 workspaces
├── Tenant: Company B
│ ├── 500 users
│ └── 3 workspaces
└── Tenant: Company C
├── 2,000 users
└── 10 workspaces
Routing:
- company-a.com → Tenant A
- company-b.com → Tenant B
- company-c.com → Tenant C
Pattern 2: Enterprise Departments¶
One company, multiple departments:
USSO Instance
└── Tenant: MegaCorp (org_megacorp)
├── Workspace: Engineering
├── Workspace: HR
├── Workspace: Finance
└── Workspace: Operations
Pattern 3: Multi-Brand¶
One company, multiple brands:
USSO Instance
├── Tenant: Brand A (org_brand_a)
├── Tenant: Brand B (org_brand_b)
└── Tenant: Brand C (org_brand_c)
Implementation Example¶
Tenant Middleware¶
from fastapi import Request, HTTPException
from typing import Optional
class TenantMiddleware:
async def __call__(self, request: Request, call_next):
# Resolve tenant
tenant = await self.resolve_tenant(request)
if not tenant:
raise HTTPException(status_code=400, detail="Tenant not found")
# Store in request state
request.state.tenant = tenant
# Process request
response = await call_next(request)
return response
async def resolve_tenant(self, request: Request) -> Optional[Tenant]:
# Try header first
tenant_id = request.headers.get("X-Tenant-ID")
if tenant_id:
return await Tenant.get(tenant_id)
# Try domain
host = request.headers.get("host", "").split(":")[0]
return await Tenant.get_from_domain(host)
Tenant-Scoped Queries¶
from fastapi import Depends, Request
def get_current_tenant(request: Request) -> Tenant:
"""Get tenant from request state"""
return request.state.tenant
@router.get("/users")
async def list_users(
tenant: Tenant = Depends(get_current_tenant)
):
# Automatically scoped to tenant
users = await User.find(
User.tenant_id == tenant.id
).to_list()
return {"users": users}
Cross-Tenant Operations (Admin Only)¶
@router.get("/admin/tenants/{tenant_id}/users")
async def list_tenant_users(
tenant_id: str,
admin: UserData = Depends(require_system_admin)
):
"""System admin can query any tenant"""
users = await User.find(
User.tenant_id == tenant_id
).to_list()
return {"users": users}
Security Considerations¶
1. Prevent Tenant Leakage¶
Always validate tenant ownership:
@router.get("/workspaces/{workspace_id}")
async def get_workspace(
workspace_id: str,
tenant: Tenant = Depends(get_current_tenant)
):
workspace = await Workspace.get(workspace_id)
# Verify workspace belongs to tenant
if workspace.tenant_id != tenant.id:
raise HTTPException(status_code=404, detail="Workspace not found")
return workspace
2. Token Validation¶
Verify token was issued by the correct tenant:
def verify_token(token: str, tenant: Tenant) -> UserData:
# Decode and verify with tenant's public key
claims = tenant.verify_jwt(token)
# Ensure tenant_id matches
if claims.get("tenant_id") != tenant.id:
raise HTTPException(status_code=401, detail="Invalid token")
return UserData(**claims)
3. Database Indexes¶
Ensure efficient tenant-scoped queries:
class User(BaseDocument):
tenant_id: str
email: str
class Settings:
indexes = [
# Compound index for tenant + email
IndexModel(
[("tenant_id", ASCENDING), ("email", ASCENDING)],
unique=True
)
]
Migration Between Tenants¶
Moving users between tenants (rare but possible):
async def migrate_user_to_tenant(
user_id: str,
target_tenant_id: str
):
"""Migrate user to different tenant"""
user = await User.get(user_id)
# Update tenant_id
user.tenant_id = target_tenant_id
# Clear tenant-specific data
user.roles = []
user.workspaces = []
# Save
await user.save()
# Revoke all sessions
await AuthSession.find(
AuthSession.user_id == user_id
).delete()
return user
Monitoring Per Tenant¶
Track metrics per tenant:
@router.post("/auth/login")
async def login(
data: LoginRequest,
tenant: Tenant = Depends(get_current_tenant)
):
# Track login attempt
metrics.increment("login.attempt", tags={"tenant": tenant.id})
try:
user = await authenticate_user(data, tenant)
metrics.increment("login.success", tags={"tenant": tenant.id})
return user
except AuthenticationError:
metrics.increment("login.failure", tags={"tenant": tenant.id})
raise
Best Practices¶
1. Use Domain-Based Routing¶
More intuitive for users:
2. Separate Branding¶
Give each tenant its own look:
3. Tenant-Level Configuration¶
Allow tenants to customize behavior:
4. Monitor Resource Usage¶
Track per-tenant usage: - Number of users - API requests - Storage used - Active sessions
5. Tenant Lifecycle¶
Support full tenant lifecycle: - Onboarding - Easy tenant creation - Configuration - Self-service settings - Monitoring - Usage dashboards - Offboarding - Data export and deletion
Troubleshooting¶
Tenant Not Found¶
# Check tenant exists
tenant = await Tenant.get_from_domain("company-a.com")
if not tenant:
print("Tenant not found for domain")
# List all tenants
tenants = await Tenant.find().to_list()
for tenant in tenants:
print(f"{tenant.name}: {tenant.domains}")
Cross-Tenant Data Leakage¶
# Always filter by tenant
users = await User.find(
User.tenant_id == current_tenant.id # ← Don't forget this!
).to_list()
Token Validation Fails¶
# Ensure token was issued for correct tenant
claims = decode_token(token)
if claims["tenant_id"] != current_tenant.id:
raise ValueError("Token tenant mismatch")
Next Steps¶
- Authentication vs Authorization - Understanding access control
- Workspaces & Roles - Organizing users
- Tokens & Sessions - Token management