GET /api/account/user-sharing only returns 1 of 2 tenantsSetup: ABP 10.3.0 commercial, .NET 10, database-per-tenant, TenantUserSharingStrategy.Shared.
Goal: one shared user logs in once, lists their tenants, then uses SwitchTenant to switch by TenantId.
Problem: A user invited into two tenants can log in and SwitchTenant into both, but
GET /api/account/user-sharing returns only one tenant.
Cause we found: in the host DB there is no TenantId = NULL master row for this user —
only two per-tenant association rows. The admin user has a NULL-tenant row; this invited user
does not. Host AbpUsers for the user:
| Id | TenantId | IsActive | Leaved | IsDeleted | |---|---|---|---|---| | 01524844… | dev tenant id | 1 | 0 | 0 | | f8bb49c3… | batu tenant id | 1 | 0 | 0 | | (no row) | NULL | — | — | — |
UserSharingAppService.GetAllListAsync → GetUserWithTenantsFromHostAsync enumerates the user's
tenants from the host master (TenantId = NULL) row; with no master, it returns only the current
tenant.
We are not manually editing any rows — we expect ABP's own invite/accept flow to create the host master row for us. Our questions:
invite-user flow
(POST /api/saas/tenants/{id}/invite-user) and accepted via
POST /api/account/user-sharing/invitation/accept. This created the per-tenant rows but no
TenantId = NULL master row. Should this flow have created the host master automatically?
Is this the wrong invite endpoint for Shared accounts (i.e. we should be calling a different
built-in invite that runs UserSharingManager.CreateUserInHostAsync), or is it a bug that the
master row is not created?/api/account/user-sharing lists all their tenants?We applied the password-grant workaround (overriding TokenController.HandlePasswordAsync to resolve
the user across tenants from the Host context via UserSharingManager.GetUsersByUserNameFromHostAsync /
GetUsersByEmailFromHostAsync, then sign in under user.TenantId). Login at /connect/token now
works and a tenant-scoped token is issued. Thank you.
Two follow-up issues remain:
EntityNotFoundException in IdentityDynamicClaimsPrincipalContributor after loginOn the token request and on subsequent authenticated calls, we see this repeatedly:
[WRN] User not found: 01524844-e59b-7751-25a3-3a21e45548bf
Volo.Abp.Domain.Entities.EntityNotFoundException: There is no such an entity.
Entity type: Volo.Abp.Identity.IdentityUser, id: 01524844-e59b-7751-25a3-3a21e45548bf
at Volo.Abp.Identity.IdentityUserManager.GetByIdAsync(Guid id)
...
at Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributorCache.GetAsync(Guid userId, Nullable`1 tenantId)
at Volo.Abp.Identity.IdentityDynamicClaimsPrincipalContributor.ContributeAsync(...)
The id 01524844-... is the invited user's tenant row (it exists in the tenant DB, not the host DB).
The dynamic-claims contributor appears to look the user up in a context/DB where that id is not found
(host/db-per-tenant), so it throws and the dynamic claims (roles/permissions) are not contributed.
After login, /api/abp/application-configuration returns:
"auth": { "grantedPolicies": {} },
"currentUser": { "isAuthenticated": false, "roles": [] }
i.e. the user has a valid token but no roles/permissions (dynamic claims didn't load).
Question: Is IdentityDynamicClaimsPrincipalContributor shared-account/db-per-tenant aware in 10.3.0?
How should we make dynamic claims resolve the user in the correct tenant DB for a shared user, so the
EntityNotFoundException stops and the user's roles/permissions are populated?
TenantUserSharingStrategy.Shared, dynamic claims enabled
(IsDynamicClaimsEnabled = true).One user account must have access to multiple tenants, with a different role in each tenant.
Concretely: a supplier's user (home tenant = "dev") is invited as a guest into a sourcing company's tenant ("batu"). They keep one login. In "dev" they have their normal role; in "batu" they get a restricted "External QC" role. They log in once, then switch tenant in-session to move between workspaces. This is exactly the use case Shared User Accounts seems designed for, on a database-per-tenant deployment.
batuhan@linkse.io)After inviting + accepting this user into two tenants, here are all the AbpUsers rows for them:
| Database | TenantId | IdentityUser.Id | EmailConfirmed |
|---|---|---|---|
| Host DB — dev association | dev e2d504fe | 01524844-… | 0 |
| Host DB — batu association | batu a0cadb7b | F8BB49C3-… | 0 |
| dev tenant DB | dev e2d504fe | 3751F3BB-… | 1 |
| batu tenant DB | batu a0cadb7b | 9488521D-… | 1 |
So for the same human in the same tenant, there are two different IdentityUser.Id values:
a host-side association row and the tenant-DB row. They are matched only by normalized
username/email — the Ids differ across databases.
sub is the HOST association id, but every tenant API resolves against the TENANT DB idLogin under Shared resolves /connect/token to Host and signs the user in under their
host association row. The issued token's sub (= AbpClaimTypes.UserId) is therefore the
host-association id (e.g. 01524844).
Every authenticated request then runs in the tenant context, where the user's id is
different (3751F3BB in dev's DB). So:
IdentityDynamicClaimsPrincipalContributorCache.GetAsync(sub, tenantId)
does GetByIdAsync(01524844) in the tenant DB → EntityNotFoundException →
empty grantedPolicies (no permissions). (This is issue #1 above.)sub directly (e.g. GET /api/identity/users/{sub},
user profile, GET /api/account/user-sharing) → 404 "There is no entity IdentityUser
with id = 01524844…", because the tenant DB only knows 3751F3BB.This is not one buggy endpoint — it is a systemic mismatch: the token carries the host id, tenant-context code expects the tenant-DB id.
We have had to add two [Dependency(ReplaceServices)] overrides to get a shared user logged in
and permissioned at all:
SharedAwareTokenController ([ReplaceControllers(typeof(TokenController))]): the stock
password grant only matches host accounts (TenantId == null), so an invited+accepted user with
only tenant-association rows is reported "no user found" → invalid_grant. Our override resolves
the user across tenants via UserSharingManager.GetUsersByUserNameFromHostAsync /
GetUsersByEmailFromHostAsync, signs in under the user's tenant, and auto-confirms the email
(the invitation already proved ownership). This fixed login.
SharedAwareDynamicClaimsCache ([ExposeServices(typeof(IdentityDynamicClaimsPrincipalContributorCache))]):
to fix empty permissions, we translate the host-association id → tenant-DB id by matching on
NormalizedUserName inside the target tenant, then call
base.GetAsync(tenantRow.Id, targetTenantId). This fixed permissions.
These two get login + permissions working, but they only patch the dynamic-claims path. Every
other endpoint that reads sub directly still 404s, because sub is still the host id. Patching
endpoint-by-endpoint is clearly the wrong layer.
sub for a shared user under db-per-tenant?Under Shared + database-per-tenant, which id is the access token's sub supposed to be — the
host-association id or the tenant-DB id? If it is supposed to be the host id, how is tenant-context
code (dynamic claims, IIdentityUserAppService.GetAsync, profile, user-sharing) meant to resolve
the correct tenant-DB user from it? Is there a built-in id-translation we are missing, or is the
intended design that the host and tenant rows share the same Id (and our data is wrong because
invite/accept created differing Ids)?
If host and tenant rows are supposed to share the same Id, what is the supported way to create a
shared membership so the tenant-DB row reuses the host id? (We invited via the Saas tenant
invite-user flow and via UserInvitationManager — both produced differing Ids.)
We need the user to switch tenant after login. We found that
AbpAccountPublicWebOpenIddictModule registers the "SwitchTenant" flow name
(GrantTypes.Add("SwitchTenant")) but does not add a handler to
AbpOpenIddictExtensionGrantsOptions.Grants (it adds LinkLogin and Impersonation, but not
SwitchTenant — it appears wired only to the MVC SwitchTenantLoginModel page). So
POST /connect/token with grant_type=SwitchTenant returns
"The specified grant type SwitchTenant is not implemented."
We worked around it by registering the handler ourselves:
options.Grants.TryAdd(
SwitchTenantExtensionGrant.ExtensionGrantName, // "SwitchTenant"
new Volo.Abp.Account.Web.ExtensionGrants.SwitchTenantExtensionGrant());
Is this the supported way to enable headless SwitchTenant token exchange for an SPA, or is there
an intended API/grant for "switch the current shared user into another of their tenants"? And after
a switch, will the new token's sub be the target tenant's id (so tenant APIs resolve), or again
a host-association id (re-triggering Question A in the target tenant)?
The user must have different roles in each tenant (normal role in dev, "External QC" in batu).
With Shared accounts + db-per-tenant, is per-tenant role assignment simply "assign roles on the
tenant-DB row" (each tenant DB has its own AbpUserRoles), and is that the supported model? The Saas
invite-user flow assigns no roles; UserInvitationManager.CreateAsync has AssignedRoles — is
that the recommended channel for per-tenant guest roles, and does it work with DirectlyAddToTenant
under Shared?
sub/id model for a shared user under database-per-tenant, so we can stop
patching id-translation in dynamic claims and (worse) per-endpoint.SwitchTenantExtensionGrant registration correct?).If the differing-Id-per-database situation is itself the bug (host and tenant rows should share one Id), please tell us the correct provisioning call so we can fix the data instead of the framework.
TenantUserSharingStrategy.Shared, dynamic claims enabled, Redis distributed lock configured.linksetenantid (options.TenantKey).Yes I enabled it. I shared my host module with link because its too long. https://justpaste.it/hvxmf