Skip to content

Switch Tenant Flow

This page documents the login flow when a user has no default tenant configured, requiring tenant selection before accessing the application.

Overview

In GrydAuth's multi-tenant architecture, users can belong to multiple tenants. When no tenant is marked as default (IsDefault = false for all UserTenant records), the user must explicitly select which tenant to use after initial authentication.

When This Flow is Triggered

ScenarioDescription
No default tenantAll UserTenant.IsDefault = false
Multiple tenants, none defaultUser belongs to several tenants with no preference
First tenant assignmentUser was just added to a new tenant
Admin removed default flagDefault was cleared by administrator

Sequence Diagram

100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar

Step-by-Step Explanation

Phase 1: Initial Login (Pre-Authentication)

1.1 Credential Validation

Standard credential validation occurs:

  • User lookup by email
  • Password verification
  • AppId validation

1.2 Default Tenant Check

The system attempts to find a default tenant:

sql
SELECT * FROM UserTenants 
WHERE UserId = @userId AND IsDefault = true AND IsActive = true

If no result is returned, tenant selection is required.

1.3 Retrieve Available Tenants

All active tenants for the user are fetched:

sql
SELECT ut.*, t.Name, t.Description 
FROM UserTenants ut
JOIN Tenants t ON ut.TenantId = t.Id
WHERE ut.UserId = @userId AND ut.IsActive = true AND t.IsActive = true

1.4 Pre-Auth Token Generation

A limited-purpose token is created:

json
{
  "sub": "user-id",
  "purpose": "tenant_selection",
  "available_tenants": ["tenant-1-id", "tenant-2-id"],
  "exp": 1706885400  // 5 minutes
}

1.5 Response

json
{
  "requiresTenantSelection": true,
  "preAuthToken": "eyJhbGciOiJIUzI1NiIs...",
  "availableTenants": [
    {
      "id": "tenant-1-id",
      "name": "Acme Corporation",
      "description": "Main company account"
    },
    {
      "id": "tenant-2-id", 
      "name": "Acme Subsidiary",
      "description": "Regional office"
    }
  ]
}

Phase 2: Tenant Selection

2.1 User Selects Tenant

The client application displays a tenant selector:

typescript
interface TenantOption {
  id: string;
  name: string;
  description?: string;
}

// UI shows dropdown or card selection

2.2 Switch Tenant Request

json
{
  "preAuthToken": "eyJhbGciOiJIUzI1NiIs...",
  "tenantId": "tenant-1-id",
  "setAsDefault": true
}

2.3 Validation

The system validates:

  1. Pre-auth token is valid and not expired
  2. Selected tenant is in the available_tenants list
  3. Tenant is still active
  4. UserTenant association is still active

2.4 Set Default (Optional)

If setAsDefault = true:

sql
-- Clear existing defaults
UPDATE UserTenants SET IsDefault = false WHERE UserId = @userId;

-- Set new default
UPDATE UserTenants SET IsDefault = true 
WHERE UserId = @userId AND TenantId = @tenantId;

User Experience

Setting a default tenant improves UX - the user won't need to select a tenant on future logins.

2.5 Full Token Generation

Now with a selected tenant, the system generates complete JWT tokens with roles and permissions for that specific tenant context.

Phase 3: Authenticated Session

After successful tenant selection, the user receives full authentication tokens and can access protected resources.

Switching Tenants During Session

Already authenticated users can switch tenants without re-entering credentials:

100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar

Error Scenarios

ErrorHTTP StatusCause
Session expired401Pre-auth token expired (5 min)
Access denied403Tenant not in available list
Tenant inactive403Tenant was deactivated
User removed403UserTenant association removed

Security Considerations

Pre-Auth Token

  • Short expiration (5 minutes)
  • Contains list of allowed tenants (prevents tampering)
  • Cannot be used for API access
  • Single-use (invalidated after tenant selection)

Cross-Tenant Access

When User.AllowCrossTenantAccess = true:

  • User can access resources across tenants
  • JWT contains additional claims for cross-tenant validation
  • Audit logs track cross-tenant access

Code Example

Client-Side Implementation

typescript
interface LoginResponse {
  accessToken?: string;
  refreshToken?: string;
  requiresTenantSelection?: boolean;
  preAuthToken?: string;
  availableTenants?: Tenant[];
}

async function handleLogin(email: string, password: string): Promise<void> {
  const response = await authApi.login({ email, password, appId });
  
  if (response.requiresTenantSelection) {
    // Store pre-auth token and show tenant selector
    sessionStorage.setItem('preAuthToken', response.preAuthToken!);
    tenantStore.setAvailable(response.availableTenants!);
    router.push('/select-tenant');
    return;
  }
  
  // Direct login - default tenant used
  authStore.setTokens(response.accessToken!, response.refreshToken!);
  router.push('/dashboard');
}

async function selectTenant(tenantId: string, setAsDefault: boolean): Promise<void> {
  const preAuthToken = sessionStorage.getItem('preAuthToken');
  
  const response = await authApi.switchTenant({
    preAuthToken,
    tenantId,
    setAsDefault
  });
  
  sessionStorage.removeItem('preAuthToken');
  authStore.setTokens(response.accessToken, response.refreshToken);
  authStore.setCurrentTenant(response.tenant);
  router.push('/dashboard');
}

// Switch tenant during active session
async function switchTenant(newTenantId: string): Promise<void> {
  const response = await authApi.switchTenant({ tenantId: newTenantId });
  
  authStore.setTokens(response.accessToken, response.refreshToken);
  authStore.setCurrentTenant(response.tenant);
  
  // Refresh current view with new tenant context
  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>
        <p>{{ tenant.description }}</p>
      </div>
    </div>
    <label>
      <input type="checkbox" v-model="setAsDefault" />
      Remember my choice
    </label>
  </div>
</template>

Released under the MIT License.