CodamAIDocs
Topicdone

Determine the tenant of a request

The six rules CIAS uses to determine the tenant from the token and header, with the results RESOLVED, NONE, AMBIGUOUS and CONFLICT.

Variants
no organization, attribute tenantno organization, no attribute → NONEattribute contradicts membership → CONFLICTheader selects own organizationexactly one organizationseveral without selection → AMBIGUOUS

What this is about

Every authenticated request runs under at most one tenant. The CIAS filter chain decides which one, before any module sees the request. CIAS calls this step resolving. It only answers the question “which tenant?”. Whether this tenant may work is checked afterwards by the tenant gate.

The resolution reads three pieces of information:

  • the organizations in the token (claim organization), that is, the dynamic tenants the person is a member of
  • the attribute tenant in the token, the permanently assigned tenant
  • the header tenant of the request, a wish of the client

The rules

The rules are checked from top to bottom. The first one that matches decides.

flowchart TB
    S([Token read]) --> O{"Organizations<br/>in the token?"}
    O -- "none" --> A{"Attribute tenant?"}
    A -- "missing" --> NONE["NONE<br/>without tenant"]
    A -- "set" --> RS["RESOLVED<br/>static tenant"]
    O -- "one or more" --> C{"Attribute tenant names<br/>none of them?"}
    C -- "yes" --> CON["CONFLICT<br/>403"]
    C -- "no or missing" --> H{"Header tenant names<br/>one of them?"}
    H -- "yes" --> R1["RESOLVED<br/>the selected one"]
    H -- "no" --> T{"Attribute tenant<br/>names one of them?"}
    T -- "yes" --> R2["RESOLVED<br/>the one from the attribute"]
    T -- "no" --> E{"exactly one?"}
    E -- "yes" --> R3["RESOLVED<br/>that one"]
    E -- "no" --> AMB["AMBIGUOUS<br/>403"]
The resolution, rule by rule
Organizations in the tokenAttribute tenantHeader tenantResult
nonemissing–NONE: the request has no tenant
nonestadtwerke-nord–RESOLVED: stadtwerke-nord, static
nordbau, suedlogistikglobex–CONFLICT: the attribute names none of the person's own organizations
nordbau, suedlogistik–suedlogistikRESOLVED: suedlogistik, selected
nordbau, suedlogistiknordbaumissing or not an own oneRESOLVED: nordbau, from the attribute
only nordbaumissingmissing or not an own oneRESOLVED: nordbau
nordbau, suedlogistikmissingmissing or not an own oneAMBIGUOUS: unclear whose data is meant

The contradiction (CONFLICT) is checked before the selection by header. Otherwise a wrongly set up person who has an attribute tenant and is also a member of organizations could be steered into any of their organizations by header.

The four results

ResultMeaningWhat the filter chain does
RESOLVEDexactly one tenantasks the tenant gate and writes the tenant into the RequestContext
NONEthe token names no tenant at allin MULTI: 403 cias.authentication.tenant-required, except for paths under /cias/**. In SINGLE and without a set operating mode: the request runs without a tenant
CONFLICTattribute and organizations contradict each other403 cias.authentication.tenant-unresolved
AMBIGUOUSseveral organizations, no selection403 cias.authentication.tenant-unresolved

In SINGLE the resolution does not happen at all: every token counts as a token without a tenant there, even one with organizations. See In SINGLE the tenant in the token does not count.

NONE on its own is not a refusal. A token without a tenant has always meant “without tenant”, and a request without a tenant may do nothing that needs a tenant. It is refused only when the installation separates tenants (MULTI). The paths under /cias/** then stay reachable, so the person can see their profile and an administrator can add the missing tenant.

The two keys tenant-unresolved and tenant-required separate two errors that are fixed in different places: “you did not select unambiguously” is fixed by the client, “you were never assigned a tenant” is fixed by an administrator in CIAS.

Selection is not a switch

The header tenant serves two purposes, and the resolution only handles the first one:

  • Selection: The header names an organization the person is a member of themselves. This is ordinary and needs no special role. The resolution selects this organization, and the person’s roles in this organization apply.
  • Switch: The header names a tenant the person is not a member of. This is privileged and happens only after resolution and gate. See Switch between tenants.

So that the selection takes effect in time, the filter chain reads the headers before the token. After all, the selected organization decides which roles the person has.

What is in the RequestContext after the resolution

The RequestContext is the store that lives for exactly one request. The resolution fills it after the gate has agreed:

EntryContent
Tenantthe resolved key, empty for NONE
Allowed tenantsthe resolved tenant, all own organizations and all entries from the attribute allowedTenants
Roles and attributesmatching the resolved tenant, see Effective roles: global or in the tenant
Description of the tenantkind (STATIC/DYNAMIC) and the organization behind it, for code that needs more than the key

The own organizations are part of the list of allowed tenants. Otherwise the persistence would refuse a person who selects their second organization.

Pitfalls

Where to go next

Sources in the code and the knowledge base
  • CIAS/cias-authentication – OrganizationTenantResolver (rules in this order), TenantResolution (RESOLVED, NONE, AMBIGUOUS, CONFLICT, denied)
  • CIAS/cias-authentication – TokenParser.admit (resolution before the gate, buildAllowedTenants), JwtSessionFilter (read header tenant before the token, tenantMissing, /cias/**), RequestAdmission
  • CIAS/cias-authentication – KeycloakOrganizationClaimReader (forms of the claim organization), CiasTokenProperties (tenant, allowedTenants, organization)
  • CIAS/cias-kernel – TenantContext, TenantRequirement
  • CIAS/cias-authentication/docs/adr – ADR-006 (sections 2–4, 6)
Search