Appearance
Switch Tenant Flow
This page documents POST /api/v1/auth/switch-tenant, used both for the initial tenant selection after login (when Login returns a Global token) and for switching tenants during an already-authenticated session.
This page was rewritten for v5
Earlier versions of this page described a "pre-auth token" with a 5-minute TTL and available_tenants embedded as a JWT claim. That design was replaced in v5 — this page now matches the actual SwitchTenantCommandHandler contract (Global token model, 2-minute TTL, availableTenants returned only in the response body). See the v4 → v5 migration guide items 15–16.
When This Flow is Triggered
| Scenario | Description |
|---|---|
| No tenant auto-resolved at login | Multiple tenants, none default, no matching preferredTenantId — Login returns a Global token |
| Mid-session switch | User already holds a Tenant (access) token and wants to switch to another tenant they belong to |
Bi-Typed Endpoint
switch-tenant accepts two token types, by design ([AllowGlobalAndTenantToken], allowlist { global, access }):
global— the token issued right after login when tenant selection is required. Single-use: it is blacklisted immediately after a successful switch.access(+tenant_idclaim) — an already-authenticated user switching tenant mid-session. Not single-use.
refresh, mfa_pending, and a missing token_type are all rejected with 401. In both cases SwitchTenantCommandHandler re-validates tenant access server-side — the token type is never itself the authorization decision.
Sequence Diagram — Initial Tenant Selection (Global Token)
100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar
Step-by-Step Explanation
1. Global Token Claims
json
{
"sub": "user-id",
"token_type": "global",
"scope": "tenant-selector-only",
"purpose": "tenant-selection",
"exp": 1751462520
}TTL is a hardcoded 2 minutes (JwtTokenService, not configurable via JwtSettings). The token carries no roles/permissions and cannot be used on any endpoint other than switch-tenant and complete-first-login.
availableTenants lives in the response body
The tenant list is not embedded in the JWT. It's returned as AuthenticationResult.AvailableTenants on the login/refresh/switch-tenant response — read it from there, never from a decoded token.
2. Switch Tenant Request
json
POST /api/v1/auth/switch-tenant
Authorization: Bearer {globalToken or accessToken}
{
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}SwitchTenantCommand accepts only tenantId. There is currently no setAsDefault field on this endpoint — a client cannot ask the server to remember the selection as part of this call. If your product needs "remember my organization" UX, treat it as a client-side preference (or a known gap to raise with the Auth team) rather than assuming the old setAsDefault: true contract still works.
3. Validation
- Token type is
global(with valid scope/purpose claims) oraccess(with atenant_idclaim). - Target tenant is resolvable for the user — direct
UserTenantassignment, or the user hasGroupAdminon the parent group of a child tenant (hierarchy inheritance rule). - Tenant and the
UserTenantassociation are still active.
4. Token Generation
A full Tenant (access) token is generated with roles/permissions for the selected tenant. For child tenants, the token also carries group_id/group_name claims. If a global token was used, it is blacklisted immediately (single-use enforcement); an access token used for a mid-session switch is not blacklisted.
Switching Tenants During an Active Session
Already-authenticated users can switch tenants without a global token:
100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar
Error Scenarios
| Error | HTTP Status | Cause |
|---|---|---|
| Session expired | 401 | Global token expired (2 min TTL) |
| Token type rejected | 401 | Token was refresh, mfa_pending, or missing token_type |
| Access denied | 403 | Tenant not accessible (no direct assignment, no GroupAdmin inheritance) |
| Tenant inactive | 403 | Tenant or UserTenant association deactivated |
Security Considerations
Global Token
- Hardcoded 2-minute TTL
- Single-use — blacklisted after a successful switch
- No roles/permissions; cannot access tenant-scoped resources
- Valid on
switch-tenantandcomplete-first-loginonly
Cross-Tenant Access
When User.AllowCrossTenantAccess = true, the user can access resources across tenants; the JWT carries additional claims for cross-tenant validation, and audit logs track cross-tenant access.
Code Example
Client-Side Implementation
typescript
interface AuthenticationResult {
token: string;
refreshToken?: string;
isGlobal: boolean;
requiresTenantSelection: boolean;
tokenType: 'Global' | 'Tenant' | 'MfaPending';
availableTenants?: TenantInfo[];
currentTenant?: TenantInfo;
}
async function handleLogin(email: string, password: string): Promise<void> {
const response = await authApi.login({ email, password });
if (response.tokenType === 'MfaPending') {
authStore.setPendingMfaToken(response.token);
router.push('/mfa-challenge');
return;
}
if (response.requiresTenantSelection) {
// Store the (short-lived) global token and show the tenant selector
authStore.setGlobalToken(response.token);
tenantStore.setAvailable(response.availableTenants!);
router.push('/select-tenant');
return;
}
// Tenant already resolved (default/single-tenant/preferredTenantId)
authStore.setTokens(response.token, response.refreshToken!);
router.push('/dashboard');
}
async function selectTenant(tenantId: string): Promise<void> {
const globalToken = authStore.getGlobalToken();
const response = await authApi.switchTenant({ tenantId }, globalToken);
authStore.clearGlobalToken();
authStore.setTokens(response.token, response.refreshToken!);
authStore.setCurrentTenant(response.currentTenant);
router.push('/dashboard');
}
// Switch tenant during an active session
async function switchTenant(newTenantId: string): Promise<void> {
const response = await authApi.switchTenant({ tenantId: newTenantId });
authStore.setTokens(response.token, response.refreshToken!);
authStore.setCurrentTenant(response.currentTenant);
window.location.reload();
}Tenant Selector Component (Vue)
vue
<template>
<div class="tenant-selector">
<h2>Select Your Organization</h2>
<div class="tenant-list">
<div
v-for="tenant in availableTenants"
:key="tenant.id"
class="tenant-card"
@click="selectTenant(tenant.id)"
>
<h3>{{ tenant.name }}</h3>
</div>
</div>
</div>
</template>No "remember my choice" checkbox
Since switch-tenant has no setAsDefault field, this component intentionally drops the old checkbox — there is currently no API-level way to persist the selection as the user's default tenant from this call.
Related Flows
- Login Flow - Standard login, including the Smart Login decision tree
- First Login - When password change is required
- MFA - When an
mfa_pendingtoken is returned instead - Multi-Tenancy Architecture - Deep dive into multi-tenancy