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 operation | Allowed tenant roles |
|---|---|
| Read details/members | owner, admin, member |
| Rename/manage ordinary members | owner, admin |
| Delete/transfer ownership | owner |
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_type | Principal | Tenant access |
|---|---|---|
human | A person, including browser sessions and human API automation | Active membership, service grant and principal mapping |
service | A machine identity, including tenant workloads and Tenant Management | Explicit 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.