Skip to content

Active Tenant as Keycloak User Attribute ​

Context ​

As part of the migration from the monolithic UserEntity MongoDB collection to Keycloak as the source of truth for user data, the active_tenant_id field needs a new home. ADR 2026_02_20_keycloak_tenant_assignment_via_groups established that "Keycloak does NOT manage active tenants" and placed this responsibility on the application database. However, with UserEntity being eliminated entirely, storing a single field in a dedicated MongoDB collection introduces unnecessary complexity when Keycloak already supports custom user attributes.

Decision Drivers ​

  • Eliminate MongoDB dependency for user state:
    The goal of the UserEntity removal is to stop duplicating user data in MongoDB. Creating a new UserActiveTenantEntity collection to hold one field undermines this goal.
  • Keycloak already owns user identity:
    Active tenant is user-scoped state. Keycloak is already the authoritative source for user identity and tenant membership (via groups). Storing the active tenant selection alongside the user record is a natural fit.
  • Immediate updates without token refresh:
    The Keycloak Admin API allows reading and writing user attributes instantly. Unlike JWT claims, attributes do not require a token refresh to take effect — the API server reads them directly via the Admin API.
  • Reduced operational complexity:
    One fewer MongoDB collection to manage, back up, and monitor.

Decision ​

The user's active tenant ID is stored as a Keycloak custom user attribute (active_tenant_id), read and written via the Keycloak Admin API.

This supersedes point 2 of ADR 2026_02_20_keycloak_tenant_assignment_via_groups ("Keycloak does NOT manage active tenants").

Implementation ​

  • Storage: Keycloak user attribute active_tenant_id (single-valued, contains the TenantMetadataEntity id — a human-readable slug matching the Keycloak group name under /tenants/).
  • Read: KeycloakAdminService.get_active_tenant_id(user_id) calls a_get_user(user_id) and extracts attributes.active_tenant_id[0].
  • Write: KeycloakAdminService.set_active_tenant(user_id, tenant_id) calls a_update_user(user_id, {"attributes": {"active_tenant_id": [tenant_id]}}).
  • Clear on tenant deletion: When a tenant is deleted, all users whose active_tenant_id matches the deleted tenant have the attribute cleared.
  • Service account: The existing aihub-api-service service account requires the manage-users realm-management role (already planned as part of the UserEntity migration).

Consequences ​

Positive ​

  • No new MongoDB collection needed for active tenant storage
  • Active tenant data lives alongside the user record in Keycloak, consistent with the direction of Keycloak as source of truth
  • Attribute updates are immediate — no token refresh cycle needed
  • Simplifies the UserEntity migration by reducing the number of replacement entities

Trade-offs ​

  • Every request that resolves the "active" tenant slug requires a Keycloak Admin API call instead of a direct MongoDB query. This adds network latency (~1-5ms internal) but can be mitigated with short-lived caching.
  • Increases the platform's dependency on Keycloak Admin API availability for the authentication hot path. If the Admin API is unreachable, active tenant resolution fails (JWKS-based token validation still works independently).
  • User attributes in Keycloak are stored as list[str] (multi-valued by default), requiring the application to read attributes["active_tenant_id"][0] rather than a simple string field.

Supersedes ​

  • Point 2 of 2026_02_20_keycloak_tenant_assignment_via_groups.md: "Keycloak does NOT manage active tenants" is no longer accurate. Keycloak now manages active tenant selection as a user attribute.
  • 2026_02_20_keycloak_tenant_assignment_via_groups.md — Tenant assignments as Keycloak groups (partially superseded)
  • 2025_12_28_keycloak_as_identity_broker.md — Keycloak as sole OIDC provider
  • 2025_12_25_local_role_management.md — Roles managed locally in MongoDB (unchanged)

Built with ❤️ in Switzerland 🇨🇭