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).TenantUserSharingStrategy.Sharedoptions.TenantKey = "linksetenantid"Relevant config:
// Domain module
Configure<AbpMultiTenancyOptions>(options =>
{
options.IsEnabled = true;
options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});
// HttpApi.Host module
Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{
options.TenantKey = "linksetenantid"; // custom header name instead of __tenant
});
admin.userA — email userA@example.com) into tenant Dev using the invitation feature.
POST /api/account/user-sharing/invitation/accept Content-Type: application/json { "token": "
The accept returned success. (Before accepting, `GET /api/account/user-sharing/invitation?token=...`
returned `requireRegister: true`, `tenantName: "Dev"`, `inviteeEmail: "userA@example.com"`.)
## What I see in the databases after accept
- In the **host** database `AbpUsers`: I see a record for `userA`, **but it has a non-null `TenantId`** (the Dev tenant's id) — I did **not** see a `TenantId = NULL` (host) record for this user.
- In the **Dev tenant** database `AbpUsers`: I see 1 record for `userA` (with the Dev `TenantId`).
(Question: under Shared accounts, should there be a host record with `TenantId = NULL` for this user? That is the part I'm unsure about — see below.)
## The problem
When `userA` tries to log in, I get **"Invalid username or password!"** (`invalid_grant`).
From the server logs, the token request resolves the tenant to **Host**, then searches for the user
in the **host** context and finds nothing:
```xml
Starting resolving tenant...
Trying to resolve tenant through 'CurrentUser'...
Trying to resolve tenant through 'AbpAccount'...
Tenant resolved by 'AbpAccount' as 'Host'.
No tenant resolved.
...
No user found matching username: "userA@example.com"
→ invalid_grant: "Invalid username or password!"
Notice my custom header resolver (linksetenantid) is never reached — the AbpAccount
contributor resolves to Host and the chain stops.
I send the login request like this:
POST /connect/token
linksetenantid: <Dev tenant id> <-- my custom tenant header
grant_type=password
username=userA@example.com
password=<password>
client_id=...&client_secret=...&scope=...
…but the linksetenantid header appears to be ignored at the token endpoint.
userA not log in after being invited+accepted under Shared accounts?TenantId = NULL? In my host DB the user only appears
with a tenant id, and there is no TenantId = NULL host record. Is the accept flow supposed to
create a host (TenantId = NULL) record, and could the missing host record be why login can't find
the user in host context?linksetenantid header
is intentionally ignored at /connect/token once Shared is enabled (the AbpAccount tenant
resolver forces Host). Is the intended flow: log in host-side (no tenant), then use the
SwitchTenant grant? If so, host login requires a host (TenantId = NULL) record — which I don't
seem to have for this user.TenantKey = "linksetenantid") supported with Shared accounts,
or does Shared mode change tenant resolution at login such that the header is not used?(login response)
{
"error": "invalid_grant",
"error_description": "Invalid username or password!",
"error_uri": "https://documentation.openiddict.com/errors/ID2024"
}
(No server-side exception — the request completes; the log shows "No user found matching username".)
After enabling Shared accounts, login resolves to Host (the linksetenantid header is not used at
the token endpoint), and the user appears to have no host (TenantId = NULL) record — only records
with a tenant id — so host-side login can't find the user. I'd like to know the correct way to make an
invited+accepted Shared user able to log in, and whether the missing TenantId = NULL host record (or
the custom tenant header being ignored at login) is the root cause.
I just started using session management with openiddict, now everything looks ok. but when I enabled LogoutFromSameTypeDevices I find that all session has same Device on DB (AbpSessions) so I wonder why and how to control what value is passed to db. second question is it possible to make custom rule about it? for example if client is Web only 1 login allowed, if client is "Mobile" you can 5 or unlimited token etc. ?
abp version 9.2.0
Yes I enabled it. I shared my host module with link because its too long. https://justpaste.it/hvxmf
Hello,
We have recently upgraded our ABP modules from version 7.0.3 to 9.1.0, and we are looking to enable and utilize Session Management in our solution.
Our project currently references the following modules:
Volo.Abp.Identity.Pro.*
Volo.Abp.IdentityServer.*
After the upgrade, we confirmed that the AbpSessions table has been created successfully. However, we are seeing logs indicating that a SessionId is not found in the database (it means never saved). It's unclear why this is happening, and we would appreciate any guidance on this.
Could you please advise on what steps we should take to correctly enable and use the session management feature?
Thank you in advance for your support.