Tenant Selection Page
August 4, 2026 · View on GitHub
When tenant selection is configured, AuthProxy serves a tenant-selection page after login until the user chooses a tenant.
The page receives tenant data through a cookie, not by calling your tenant endpoint directly.
Flow
- Authenticated request arrives without a
.cratis-tenantcookie. - AuthProxy calls the configured
Selection.Options.TenantsEndpoint. - If exactly one tenant is returned, AuthProxy sets
.cratis-tenantimmediately, removes any.cratis-tenantscookie, and redirects to the original URL (no selection page shown). - If more than one tenant is returned, AuthProxy writes
.cratis-tenants(URL-encoded JSON array) as a session cookie and servesselect-tenant.html— but only to a browser navigating to a page. Every other caller is refused with403instead, because a chooser page delivered as200reads as the data they asked for. See Unauthenticated responses. - User clicks a tenant option.
- Browser navigates to
/.cratis/select-tenant?tenantId=<id>&returnUrl=<path>. - AuthProxy validates the selected
tenantIdagainstTenantsEndpointand sets.cratis-tenant. - AuthProxy redirects back to
returnUrl. The.cratis-tenantscookie is retained so the application can offer an in-app tenant switcher.
Switching tenants after selection
For a user with more than one tenant, .cratis-tenants is written as a session cookie and is
not deleted when a tenant is selected. It therefore remains available for the rest of the browser
session, which lets the application's toolbar decide whether to show a "switch tenant" control (show it
only when the cookie lists more than one tenant).
To switch, the toolbar navigates to the same selection endpoint used by the selection page:
/.cratis/select-tenant?tenantId=<id>&returnUrl=<current path>
Every switch re-validates the requested tenantId against TenantsEndpoint, so a stale cookie can
never grant access to a tenant the user is no longer a member of — an unknown tenantId is rejected
with 400 Bad Request.
Periodic re-validation of the selected tenant
.cratis-tenant is a session cookie, but it is also not trusted indefinitely within a session:
AuthProxy re-validates the selected tenant against TenantsEndpoint when the configured
Cratis:AuthProxy:Session:TenantRevalidationInterval (default 10 minutes) has lapsed. A successful
re-validation is cached in memory, so the endpoint is not called on every request.
When the endpoint answers authoritatively that the tenant is no longer available to the user, the
.cratis-tenant and .cratis-tenants cookies are deleted and the request is replayed without them —
the user lands back in the regular selection flow (or the no-tenant handling when nothing remains).
Revoked tenant access therefore takes effect within the interval, without waiting for the browser
session to end. Transport failures fail open so a transient backend outage cannot lock users out;
the next lapse of the window retries.
A user with exactly one tenant never receives .cratis-tenants (it is removed when the single
tenant is auto-selected), so no switcher is shown for single-tenant users.
Endpoint response shape
TenantsEndpoint must return JSON with one object per selectable tenant:
[
{
"id": "some string",
"name": "some string"
}
]
Cookie shape used by the page
AuthProxy writes .cratis-tenants with the same shape:
[
{
"id": "studio",
"name": "Cratis Studio"
},
{
"id": "sales",
"name": "Sales Portal"
}
]
Building a custom select-tenant.html
select-tenant.html is a normal overridable page in PagesPath, exactly like the other built-in pages.
Minimum requirements:
- Read
.cratis-tenantsfromdocument.cookie. - Parse JSON and render
name. - Link each item to
/.cratis/select-tenant?tenantId=<id>&returnUrl=<current path>.
Example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Select tenant</title>
</head>
<body>
<h1>Select tenant</h1>
<ul id="tenants"></ul>
<p id="error" hidden>Unable to load tenant options.</p>
<script>
(function () {
function getCookie(name) {
var pattern = new RegExp('(?:^|; )' +
name.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1') + '=([^;]*)');
var match = document.cookie.match(pattern);
return match ? decodeURIComponent(match[1]) : null;
}
var json = getCookie('.cratis-tenants');
var tenants;
try { tenants = JSON.parse(json); } catch (_) { tenants = null; }
if (!tenants || tenants.length === 0) {
document.getElementById('error').hidden = false;
return;
}
var returnUrl = window.location.pathname + window.location.search;
var list = document.getElementById('tenants');
tenants.forEach(function (tenant) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = '/.cratis/select-tenant?tenantId=' +
encodeURIComponent(tenant.id) +
'&returnUrl=' + encodeURIComponent(returnUrl);
a.textContent = tenant.name;
li.appendChild(a);
list.appendChild(li);
});
}());
</script>
</body>
</html>