Skip to main content
Version: Next

Shared tenancy contracts

@tsg-dsp/tenant-dtos defines the version 1 contracts used by the SSO Bridge, Wallet, Control Plane and future Tenant Management service. A tenant is an organization/workspace; users can belong to several tenants. Configuration and authorization revisions are independent, so membership updates do not regenerate Wallet keys or participant identity.

Identity and administration​

Canonical tenant UUIDs are shared across services. Existing domain foreign keys continue to reference each service's local tenant ID. The additive Wallet and Control Plane migrations leave canonicalTenantId null for existing rows. TenantContextResolver.bindCanonical(localId, canonicalId) imports an explicitly confirmed mapping without changing local IDs. Mappings are unique, immutable, and reserved after deletion. Do not pair organizations using matching slugs alone.

Principals are identified by the exact (issuer, subject) pair. Authorization projections carry explicit local subjects and ownership aliases, including legacy user:<id> aliases where applicable. Never merge identities by username or email. Slugs are immutable and never recycled; names remain editable. Deleted tenants are not restored by old initTenants configuration.

Tenant operationAllowed tenant roles
Read details/membersowner, admin, member
Rename/manage ordinary membersowner, admin
Delete/transfer ownershipowner

Existing consumers require membership of the addressed tenant for administrative operations, including bearer callers. Owner promotion/removal is rejected; the authoritative ownership-transfer operation will be implemented by Tenant Management. Service permissions remain separate from organization roles: a local Wallet administrator is not automatically an organization administrator.

Headers and paths select tenants but do not authorize access. Conflicting selectors fail with 400. Existing X-Tenant-Id values are local IDs; tenant_id token claims are canonical IDs. Tenant discovery needs no selection; human automation must select the tenant explicitly. Legacy Control Plane configuration memberships are reconciled for authenticated human bearer callers as well as sessions, including tenant discovery. Configuration imports only create missing memberships; they do not restore revoked memberships or overwrite existing roles, including after restart. Explicit administrator re-grants can restore a revoked membership. Auth-disabled deployments retain their existing local administration behavior.

Actor identity and authority​

The authenticated actor contract has two values:

actor_typePrincipalTenant access
humanA person, including browser sessions and human API automationActive membership, service grant and principal mapping
serviceA machine identity, including tenant workloads and Tenant ManagementExplicit service grant and principal mapping; an existing revoked membership still denies access

The OAuth client identifies the application used to obtain a token; it does not make a human caller a service. Browser sessions always normalize to human. For compatible machine tokens without actor_type, a client-credentials grant or a client identifier (client_id, legacy clientId, or azp) matching the subject can establish service identity; human identity claims take precedence over the identifier fallback. Unknown actor values are rejected.

Actor kind grants no platform authority. Tenant Management is a service whose exact issuer/subject must be registered for each receiving service, audience and provisioning operation. An ordinary service cannot provision merely by using actor_type: service. Human platform administrators remain human; global administration requires an explicit operator policy, independent of tenant roles. OAuth client policies can distinguish shared browser clients, tenant workloads and operator-controlled clients without introducing additional actor types.

The #432 implementation must issue human/service claims and derive global service authority from its registered client policy when rebasing onto this contract. It must not replace checks for the former platform actor value with a check that accepts every service.

Authorization projections​

TenantProjectionModule supplies persistent snapshots to Wallet and Control Plane. Each snapshot describes one (tenantId, serviceInstanceId) target. Tenant Management delivers the same authorization revision/content and deadline to the target service and to SSO. SSO uses the target's snapshot to issue tokens; the target uses its copy to enforce current authorization. Wallet and Control Plane receive only their own snapshots. SSO keeps separate copies for the registered audiences it serves; there is no separate aggregate SSO grant format and no second membership authority.

TenantProjectionService.apply checks a snapshot against an explicitly selected target. Consumers pin that target to their own service instance. SSO must first check that the target belongs to the tenant's registered service bindings, after authenticating the provisioning caller. A payload's serviceInstanceId alone never authorizes delivery. Database uniqueness and compare-and-swap updates prevent concurrent or reordered delivery from restoring an older revision. Same-revision content changes conflict. A same-content heartbeat can extend validUntil within the 60-second freshness bound; replaying an old snapshot cannot extend its original deadline.

principalMappings.localSubject and ownership aliases remain local to the target service even in SSO's copy. For example, one (issuer, subject) can map to legacy-wallet-user in Wallet and legacy-cp-user in Control Plane. SSO resolves the authenticated login identity using the shared principal and issues from its target grant; it must not require a Wallet or Control Plane local subject to equal its own login ID. Only the SSO-targeted snapshot uses SSO-local mappings.

requireTenantAccess checks the verified actor's canonical tenant, single service audience, current authorization revision, membership and bounded service grant. It returns the explicit principal mapping for local ownership/resource checks. Apply existing ABAC and repository scoping using the resolved local tenant ID; this helper does not replace those checks. Services require explicit grants and mappings, and revoked memberships remain denied.

Snapshots expire within 60 seconds and inactive tenants fail closed. Consumers must receive authenticated heartbeats even when entitlements are unchanged; manager outages therefore stop protected managed operations after expiry. Public DID/credential-status verification uses separate lifecycle rules.

Provisioning callers must satisfy requireProvisioningAccess with an operator-registered issuer/subject, receiving service instance, audience and operation. Ordinary tenant admins and delegation headers cannot satisfy that policy. Configuration validation, durable operation receipts, outbox delivery, reconciliation and managed endpoint activation are implemented in #433; the foundation does not require a running manager.

Managed browser authorization​

The agreed consumer integration in #433 keeps the global login session separate from API authorization. The session establishes the user's identity; an API access token authorizes that principal for one selected canonical tenant and one service audience. An ID token or a session's copied permission array does not authorize managed tenant operations.

The application backend retains verified access/refresh tokens in server-side session storage, associated with the issuer, subject, client, canonical tenant and audience. Tokens must not be exposed through user-info responses, browser storage or logs. On login/callback, verify ID and access tokens separately and require them to identify the same principal. On each cookie-authenticated API request, select the correctly bound access token, validate it, then run the same fresh projection, tenant, audience, revision and resource checks as for a bearer request. Clear tokens on logout or identity changes.

Selecting another tenant obtains a new authorization-code grant for that tenant and audience using the existing SSO login session where possible. A selector cannot retarget a token. Refresh rotates credentials for the original binding; it cannot change tenant/client/audience or increase the granted scope. Serialize concurrent refreshes for a binding so normal parallel requests do not trigger refresh replay handling. Missing, expired or stale authorization must be renewed or denied; never fall back to the global session or a service's own credentials.

#432 supplies tenant/audience-bound issuance, client permission ceilings and code/refresh restrictions. Its local SSO administration can resolve session grants against SSO's own snapshot; that does not make its global session an API credential for Wallet or Control Plane. #433 wires server-held tokens into the consumer callbacks, guards and tenant switchers, retaining explicit per-request tenant selection so multiple tabs and late responses cannot cross tenants. These managed paths remain inactive in the foundation's legacy rollout.

Compatible rollout​

auth.tokenValidationMode defaults to legacy for existing deployments. JWTs always verify their discovered/configured issuer. Set auth.expectedAudience to enforce a receiving audience; omitted legacy audiences preserve existing client behavior. Explicit ID/refresh token types are rejected, and a JWT verification failure never falls back to introspection. Browser login validates the returned ID token separately against the issuer and login client ID; an opaque API access token does not require introspection for that login. Access-token-only login responses continue through API token validation. Untyped legacy JWTs require an operator-registered API audience in auth.legacyAccessTokenAudiences, excluding the login clientId; current SSO tokens already have an explicit access-token type. JWTs require expiry in either mode. Legacy opaque introspection responses may omit issuer/audience/type; supplied claims and configured constraints are checked.

strict requires API JWT type at+jwt (or application/at+jwt), expiry, subject, issued-at, issuer and expectedAudience (defaulting to clientId). Strict opaque tokens require active, subject, issuer, audience, expiry and explicit access-token type. Signaling's existing audience, peer and tenant-slug rules remain intact.

Merge the foundation (#463), SSO issuance/projections (#432), then authoritative Tenant Management and consumer integration (#433). Enable the managed access helpers only after the complete lifecycle is available. Import existing relationships and service-specific grants explicitly, preserve published URLs/DIDs/keys, and resolve ambiguous owners with the operator. Managed mode must not fall back to global legacy tokens or independent membership writers.