Skip to content
bitzorcas
中EN

Guide

Multitenancy tenant switching and impersonation

Distinguish transport hints, Host operate-as, and formal grant-backed tenant impersonation, including token, audit, authorization, and current implementation gaps.

Last updated

“Switch tenant” can describe three mechanisms with very different security properties. Treating them as interchangeable hides both audit gaps and data-isolation risk.

1. Three distinct mechanisms

MechanismIntended callerEvidenceCurrent runtime snapshotRevocation
X-Tenant or pathauthenticated user/applicationmust equal authenticated tenantordinary CurrentTenantauthentication lifecycle
X-Operate-As-Tenant / DebugHost or Developmentcaller type or environment onlyordinary CurrentTenantnone per session
formal tenant impersonationprivileged operatorTenantImpersonationGrant and tokenfour-field impersonation snapshotexpiry or grant deletion

The first is a confirmation hint, the second is a privileged override, and only the third is a formal impersonation session.

2. Formal grant model

TenantImpersonationGrant is owned by the operator tenant and records:

  • operator tenant and operator user;
  • target tenant;
  • validity window;
  • Scope;
  • MaxSessions;
  • RequiresStepUp.

The aggregate’s TenantId equals the operator tenant, not the target. This matters for owner-local storage and authorization.

Operator tenant and user

Impersonation grant

Target tenant

Validity window

Scope / step-up / sessions

Short-lived token

CurrentTenant audit tuple

3. What token issuance enforces

TenantImpersonationTokenService.IssueAsync currently:

  1. rejects nested user impersonation;
  2. rejects an empty target;
  3. rejects switching to the operator’s own tenant;
  4. requires a grant returned by the store;
  5. requires the grant time window to be active;
  6. caps token expiry at the earlier of two hours and grant expiry.
Issue from a trusted operator context
public static class TenantImpersonationErrors
{
public static readonly Error Nested =
Error.Forbidden("TenantImpersonation.Nested", "Nested impersonation is forbidden.");
}
if (currentUser.IsImpersonating)
{
// Reject before grant lookup so no nested session can be issued.
return Result.Failure<TenantImpersonationTokenDescriptor>(
TenantImpersonationErrors.Nested);
}
// The service must receive operator facts from authentication, not request-body fields.
var issued = await tokenService.IssueAsync(
operatorTenantId: currentTenant.Tenant.ActorTenantId,
operatorUserId: currentUser.UserId!.Value.ToString(CultureInfo.InvariantCulture),
targetTenantId: command.TargetTenantId,
cancellationToken);

The result is a descriptor, not a signed JWT. It contains operator, target, grant, scope, issued time, and expiry facts.

4. Current authorization gaps

The model contains controls that the service does not yet enforce:

ControlStoredEnforced during issuance or request
validity windowyesyes
same-tenant and nested restrictionsderivedyes
Scopeyesreturned in descriptor only
RequiresStepUpyesno
MaxSessionsyesno
target tenant existence/statustarget idno
operator user statusoperator idno
grant owner equals supplied operator tenantowner TenantIdnot compared

IsStillAuthorizedAsync(grantId, operatorTenantId) validates non-empty arguments, loads by GrantId, and checks time. It does not compare the loaded grant’s owner TenantId with the supplied operator tenant.

5. Request-time middleware

When CurrentTenant.IsImpersonating is true, TenantImpersonationTokenMiddleware:

  • returns 401 TenantImpersonation.Expired when the snapshot expiry has passed;
  • returns 401 TenantImpersonation.Unavailable when the service is not registered;
  • returns 401 TenantImpersonation.Revoked when the grant is missing or no longer active.

It delegates when no formal impersonation tuple is installed. Consequently, X-Operate-As-Tenant does not activate these checks.

6. Missing HTTP bridge

The source tree contains the grant, service, descriptor, and validation middleware. It does not contain a complete HTTP bridge that:

  1. exposes an authorized issue endpoint;
  2. requires step-up when configured;
  3. signs the descriptor into claims;
  4. authenticates those claims on later requests;
  5. reconstructs the full CurrentTenant tuple;
  6. records issue, use, revocation, and exit events.
Tenant-owned storeRequest middlewareToken signerGrant storeAuthorization and step-upIssue endpointTenant-owned storeRequest middlewareToken signerGrant storeAuthorization and step-upIssue endpointOperatortarget tenant and reasonpermission, approval, step-upvalidate owner, target, scope, windowvalidated descriptorshort-lived signed tokenbusiness request with tokenrecheck grant and sessioneffective target tenant onlyOperator

This diagram is the target protocol, not a claim that the current Host already implements it.

7. Host operate-as data-surface defect

The override middleware installs new CurrentTenant(targetTenantId, OfficeId: user.OfficeId). It does not set original tenant, impersonator, grant, or expiry. It also skips the status guard.

Both ORM providers then see an authenticated Host and bypass tenant filtering entirely. The effective target value exists, but ordinary Store queries remain platform-wide.

The repair should separate two capabilities:

  • operate as target tenant: normal Stores filter to the target and every resource uses that EffectiveTenantId;
  • platform-wide query: a dedicated port guarded by a separate permission, reason, approval, limits, and audit.

8. Audit contract

A formal impersonation audit record should preserve:

FieldMeaning
EffectiveTenantIdtenant whose data was accessed
ActorTenantIdoperator’s home tenant
EffectiveUserIdbusiness user identity, if user impersonation also applies
ImpersonatorUserIdprivileged human operator
GrantId and session idauthorization and revocation evidence
Scope and reasonintended operational boundary
CorrelationId and TraceIdend-to-end request chain

The audit trail should record both successful use and rejection. Do not store the complete token.

9. Endpoint contract example

Target contract for a formal issue endpoint
group.MapPost("/tenant-impersonation/sessions", async (
StartTenantImpersonationRequest request,
ICurrentUser currentUser,
ITenantImpersonationOrchestrator orchestrator,
CancellationToken cancellationToken) =>
{
// The orchestrator owns permission, approval, step-up, grant, target-status, and audit checks.
var result = await orchestrator.StartAsync(
operatorUserId: currentUser.UserId!.Value,
targetTenantId: request.TargetTenantId,
reason: request.Reason,
cancellationToken);
// Return a short-lived session token; never expose the grant aggregate itself.
return result.ToHttpResult();
})
.RequireAuthorization("platform.tenancy.impersonation.start");

This is an integration design based on existing types, not source copied from an existing endpoint.

10. Security test matrix

  • grant owner mismatch is rejected even when GrantId exists;
  • suspended, deactivated, or unknown target tenant is rejected;
  • RequiresStepUp cannot be bypassed;
  • Scope limits both endpoint permission and data surface;
  • MaxSessions is enforced atomically across instances;
  • deleting the grant invalidates the next request;
  • operate-as and formal impersonation filter both ORM providers to the target;
  • cache keys, files, search, and reports use EffectiveTenantId;
  • audit preserves operator and target facts.

11. Source review

Terminal window
# Review grant fields, issuance checks, and request-time validation as one protocol.
rg -n "TenantImpersonationGrant|IssueAsync|IsStillAuthorizedAsync" \
src/Platform/Identity src/Hosts/BitzOrcas.Api -g '*.cs'
# Confirm whether an HTTP issue and claim-reconstruction bridge now exists.
rg -n "TenantImpersonationTokenDescriptor|TenantImpersonationGrantId|RequiresStepUp" \
src/Hosts src/Platform/Identity -g '*.cs'
# Review every privileged override alongside ORM Host bypass.
rg -n "X-Operate-As-Tenant|IsTenantFilterBypass|isHostBypass" \
src/Hosts src/Framework -g '*.cs'

Back: CurrentTenant and persistence · Next: Background work and resources

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%