Hello!
We're migrating from ABP 10.1 to ABP 10.3 because we want to use the new User Shared Accounts feature. We were able to complete the migration successfully but we have two issues we would like some help with:
After successful login we tried the "switch tenant" option but it fails with error 405. Here's a screenshot:
. No errors in the logs, just this:
[17:56:06 INF] Request starting HTTP/2 POST https://localhost:44337/Account/SwitchTenantLogin - application/x-www-form-urlencoded 105
[17:56:06 DBG] Starting resolving tenant...
[17:56:06 DBG] Trying to resolve tenant through 'CurrentUser'...
[17:56:06 DBG] Tenant resolved by 'CurrentUser' as 'Host'.
[17:56:06 DBG] No tenant resolved.
[17:56:06 DBG] Get dynamic claims cache for user: 49de4b5d-8fd0-c336-518d-3a213b12d893
[17:56:06 INF] Executing endpoint '405 HTTP Method Not Supported'
[17:56:06 INF] Executed endpoint '405 HTTP Method Not Supported'
[17:56:06 INF] Request finished HTTP/2 POST https://localhost:44337/Account/SwitchTenantLogin - 405 0 null 4.3153msIn the previous version we had a list of pre-defined users that were seeded to every tenant using
IDataSeedContributorso when ever we created a tenant it will have some default users ready to use. This process now fails because users have to be globally unique, do you have any recommendation on how to achieve the same thing with this new version? I know we can invite the users to the tenant but that's a manual process and it requires that the user accepts the invitation in their email. We want the creation of these default users to be automatic.
Thank you in advance for your help!
7 Answer(s)
-
0
Issue #1 (Switch tenant → 405 on
/Account/SwitchTenantLogin)In ABP, the tenant switch UI in the Account layout posts to a Razor Page endpoint. A
405 HTTP Method Not Supportedtypically means the POST target endpoint is not registered in the app that receives the request.In a tiered / separated auth server setup, the
/Account/*endpoints are served by the AuthServer (or the project hosting the Account module UI), not by the Blazor Server UI project. Your log shows the request is going to:https://localhost:44337/Account/SwitchTenantLoginSo the fix is: ensure the “Switch tenant” form/action points to the AuthServer host where the Account module is actually running (and that the AuthServer has the Account web pages enabled).
Practical steps (what to check/change):
- Open the tenant switch from the login/account page that is rendered by your AuthServer, not from the Blazor UI host.
- Verify your Blazor UI project is not trying to serve
/Account/*routes itself. - If you customized the theme/layout or account pages, ensure the tenant switch posts to the ABP page endpoint (commonly
/Abp/MultiTenancy/TenantSwitchModalin newer UI implementations) that exists in the AuthServer host.
A verified related support thread shows the tenant switch UI is controlled from the theme Account layout, and demonstrates overriding it to post to
/Abp/MultiTenancy/TenantSwitchModal(rather than a custom/Account/SwitchTenantLoginpath) in LeptonX layout override:- https://abp.io/support/questions/9283
Issue #2 (Seeding “default users per tenant” with globally-unique users / Shared Accounts)
With User Shared Accounts, the same user can be related to multiple tenants, but the user identity becomes globally unique, so the old approach “create the same usernames in every tenant” will conflict.
Recommended approach to keep it automatic:
- Seed the default users only once in the Host context (globally).
- On each tenant creation (or tenant seed), programmatically associate/invite those existing host users to the new tenant rather than recreating them.
Conceptually, this becomes a 2-step seed:
- Global seed (host): create users
support@...,ops@..., etc. once. - Per-tenant seed: add membership/assignment records that grant them access to the tenant.
I don’t have a verified ABP 10.3 source/doc snippet in the available context that shows the exact service/API name to “auto-accept” an invitation (without email confirmation) for Shared Accounts in tiered Blazor Server. However, the correct direction is not duplicating
IdentityUserrecords per tenant anymore—use the shared-account membership mechanism instead.Multi-tenancy background (verified):
- https://abp.io/docs/10.3/framework/architecture/multi-tenancy/
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
- Issue 1: I can confirm that
https://localhost:44337/Account/SwitchTenantLoginis a post to the blazor server project. How do I make it post the auth server project? - Issue 2: This approach sounds good to me but I would need to know how to programmatically invite and accept users in the tenant.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Issue 1: I can confirm that
-
0
Hi,
Two separate issues — let me address each.
Issue 1: Switch Tenant returns 405
The
SwitchTenantmodal in the Blazor Server / WebApp project posts to a relative path/Account/SwitchTenantLogin, which means the form is submitted to the Blazor host itself (port 44337 in your case). But theSwitchTenantLoginRazor Page is only registered on the AuthServer (theVolo.Abp.Account.Pro.Public.Webmodule), so the Blazor host has no handler and returns 405.The Blazor
SwitchTenantModalalready supports an absolute URL — it just needsAbpAccountSwitchTenantOptions.LoginUrlto be configured. The default app template doesn't wire this up (it does wireAbpAccountLinkUserOptions.LoginUrlright above, which is why Link Account works but Switch Tenant doesn't).Add this in your
*BlazorModule.cs(or*BlazorServerModule.cs)ConfigureServices, next to the existingAbpAccountLinkUserOptionsblock:using Volo.Abp.Account.SwitchTenant; Configure<AbpAccountSwitchTenantOptions>(options => { options.LoginUrl = configuration["AuthServer:Authority"]; });Make sure your OIDC config has
options.SaveTokens = true(the default template already has it), otherwise the access_token can't be attached to the form action.We'll fix the template to include this by default.
Issue 2: Seeding default users to every new tenant under Shared Accounts
The old approach (
IDataSeedContributorseeds an admin/support/ops user into every new tenant) doesn't translate to Shared mode, and not just mechanically — it's incompatible with the design.Under Shared mode, an email/username pair identifies one person for the whole system. That person can belong to many tenants, but two different people can't share an email. In
AbpUsersthey show up as a host root row (TenantId = null) plus one tenant-shadow row per tenant they belong to — all those rows are the same identity. There's no such thing as "tenant A's support user" and "tenant B's support user" as two separate accounts; there is onesupport@yourcompany.comperson, and tenants either have an association with that person or they don't.That's the reason SaaS removed the auto-create-admin behavior.
TenantAppService.CreateAsyncnow publishesInviteUserToTenantRequestedEto { DirectlyAddToTenant = false }, which emails the admin and waits for them to opt in. Joining a tenant is the user's choice, not the system's. The invitation record is the audit trail of that choice.So the right pattern in Shared mode is:
- Create the
support@yourcompany.com/ops@yourcompany.comusers once in the host (a host-onlyIDataSeedContributorthat callsIdentityUserManager.CreateAsync(...)withTenantId = nullis fine, or let them self-register). This produces the single host root per identity that the model expects. - Each tenant gets these users via the normal invitation flow — either you (the host admin) invite them from the SaaS host page after creating the tenant, or your operations workflow does it. The user accepts the invitation, and the tenant-shadow record gets created.
If you bulk-create tenants and find clicking Invite for each one tedious, the realistic answer is to script the host-side action — call SaaS's
InviteUserAsync(or publishInviteUserToTenantRequestedEto) right after your tenant-creation script for each default email. The user still opts in via the email; you've just automated the host-side invitation send, not the user's consent.I don't recommend writing a
TenantCreatedEtohandler that programmatically auto-accepts invitations on behalf of these users. The primitives to do it exist (UserInvitationManager.CreateAsync+UserSharingManager.AcceptInviteAsyncis whatUserSharingManager.AddPendingUserToTenantAsyncuses internally for theDirectlyAddToTenant = trueself-registration path), but applying them to a fixed default-user list means you're forging consent for every new tenant — which defeats the user-sovereignty guarantee the invitation flow was added to provide, and leaves no audit story when one of those users later changes role or leaves.I verified Issue 1 end-to-end on a fresh tiered Blazor Server template (ABP 10.3.0, Shared mode): the 405 disappears after wiring
AbpAccountSwitchTenantOptions.LoginUrl.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - Create the
-
0
Hi Re: Issue 1: After adding that change it no longer attempts to send the post to the blazor server app but it doesn't work, after clicking switch it just shows the auth server
. These are the logs:
[22:58:39 ERR] Invalid RedirectUrl: https://localhost:44337/Account/Challenge, Use AppUrlProvider to configure it!
[22:58:39 INF] Executed handler method OnPostAsync, returned result Microsoft.AspNetCore.Mvc.RedirectResult.
[22:58:39 INF] Executing RedirectResult, redirecting to /.
[22:58:39 INF] Executed page /Account/SwitchTenantLogin in 159.043ms
[22:58:39 INF] Executed endpoint '/Account/SwitchTenantLogin'
[22:58:39 INF] Request finished HTTP/2 POST https://localhost:44335/Account/SwitchTenantLogin?access_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IkI1MUNGRTg3N0QwNTE3NTE4MDZGQkM4RjdFNTU4MUMzOTZBMUU3NEIiLCJ4NXQiOiJ0UnotaDMwRkYxR0FiN3lQZmxXQnc1YWg1MHMiLCJ0eXAiOiJhdCtqd3QifQ.eyJpc3MiOiJodHRwczovL2xvY2FsaG9z...
[22:58:39 INF] Request starting HTTP/2 GET https://localhost:44335/ - null nullRe: Issue 2: Than you for the detailed explanation, I will attempt to implement the pattern you described.
Thank you.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
That's the second layer of the Switch Tenant flow. Your log line is the giveaway:
[ERR] Invalid RedirectUrl: https://localhost:44337/Account/Challenge, Use AppUrlProvider to configure it!After
SwitchTenantLogin.OnPostAsyncsigns the user into the new tenant, it callsRedirectSafelyAsync(ReturnUrl, ...), which only allows return URLs whose origin is inAppUrlOptions.RedirectAllowedUrls. Your Blazor hosthttps://localhost:44337isn't in that list on the AuthServer side, so the redirect falls back to~/— the AuthServer home page, which is what you're seeing.The template wires the list from
App:RedirectAllowedUrlsinappsettings.json:// TestApp.AuthServer/<AuthServerModule>.cs Configure<AppUrlOptions>(options => { options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"]; options.RedirectAllowedUrls.AddRange( configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>()); });Fix
In your AuthServer project's
appsettings.json, add your Blazor host URL toApp:RedirectAllowedUrls:{ "App": { "SelfUrl": "https://localhost:44335", "RedirectAllowedUrls": "https://localhost:44337,...your existing entries..." } }Then restart the AuthServer. After the switch completes, the AuthServer will redirect back to
https://localhost:44337/Account/Challengeand re-establish the Blazor session under the new tenant.While you're there, make sure the same Blazor URL is also in
App:CorsOrigins(usually already added by the template). If you have other UI hosts pointing at this AuthServer, add their origins too — the check usesStartsWith, so a single entry likehttps://localhost:44337covers any path under that origin.Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
This worked. Thank you @maliming.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)