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

  1. Authenticated request arrives without a .cratis-tenant cookie.
  2. AuthProxy calls the configured Selection.Options.TenantsEndpoint.
  3. If exactly one tenant is returned, AuthProxy sets .cratis-tenant immediately, removes any .cratis-tenants cookie, and redirects to the original URL (no selection page shown).
  4. If more than one tenant is returned, AuthProxy writes .cratis-tenants (URL-encoded JSON array) as a session cookie and serves select-tenant.html — but only to a browser navigating to a page. Every other caller is refused with 403 instead, because a chooser page delivered as 200 reads as the data they asked for. See Unauthenticated responses.
  5. User clicks a tenant option.
  6. Browser navigates to /.cratis/select-tenant?tenantId=<id>&returnUrl=<path>.
  7. AuthProxy validates the selected tenantId against TenantsEndpoint and sets .cratis-tenant.
  8. AuthProxy redirects back to returnUrl. The .cratis-tenants cookie 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"
  }
]

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:

  1. Read .cratis-tenants from document.cookie.
  2. Parse JSON and render name.
  3. 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>