Environment
- ABP Commercial 9.3.7
- UI: Angular
- Authentication: separate Auth Server using OpenIddict
- Multi-tenancy: domain/sub-domain tenant resolver
- Host: mydomain.com
- Tenants: {tenant}.mydomain.com
- Configured with AddDomainTenantResolver and AbpOpenIddictWildcardDomainOptions
Goal
When a user switches to a linked account in another tenant, the browser should be redirected to that tenant's sub-domain so the application loads under the correct tenant URL.
1. Which options class should be used for Angular + OpenIddict?
The documentation and forum posts (for example, #7425) configure AbpAccountOptions:
Configure<AbpAccountOptions>(options =>
{
options.IsTenantMultiDomain = true;
options.GetTenantDomain = (httpContext, info) =>
Task.FromResult(/* tenant URL */);
});
In our Angular + OpenIddict setup, this had no effect. However, configuring AbpAccountOpenIddictOptions works:
Configure<AbpAccountOpenIddictOptions>(options =>
{
options.IsTenantMultiDomain = true;
options.GetTenantDomain = (httpContext, info) =>
Task.FromResult(/* tenant URL */);
});
We could not find any documentation for AbpAccountOpenIddictOptions.
2. IsTenantMultiDomain causes a duplicate tenant_domain parameter
After enabling IsTenantMultiDomain, the grant_type=LinkLogin request to /connect/token fails with HTTP 500:
A parameter with the same name already exists. (Parameter 'name')
The exception originates from LinkLoginExtensionGrantProcessJsonResponse. From debugging, it appears that:
- LinkLoginExtensionGrant adds tenant_domain.
- LinkLoginExtensionGrantProcessJsonResponse attempts to add tenant_domain again, causing the exception.
Our workaround is:
PreConfigure<OpenIddictServerBuilder>(builder =>
{
builder.RemoveEventHandler(
LinkLoginExtensionGrantProcessJsonResponse.Descriptor
);
});
After removing the handler, tenant_domain is still included in the response and account switching works correctly.
3. Cross-domain switch passes tokens in the URL
For cross-sub-domain account switching, Angular's LinkLoginHandler redirects with the tokens embedded in the query string (single line, shown wrapped for readability):
https://tenant.mydomain.com?handler=linkLogin&token={"access_token":"...","refresh_token":"...","access_token_stored_at":...,"expires_at":...}
The destination application reads the token payload from the query parameters and stores it in localStorage.
Questions
- Is AbpAccountOpenIddictOptions the correct and supported configuration for Angular + OpenIddict linked-account switching?
- Are the duplicate tenant_domain exception and the need to remove LinkLoginExtensionGrantProcessJsonResponse a known issue, or is there a recommended fix?
- Is redirecting to the target tenant sub-domain and passing tokens in the URL the recommended approach for cross-tenant linked-account switching? If not, does ABP provide a more secure alternative that achieves the same result?
1 Answer(s)
-
0
Hi,
1.
AbpAccountOpenIddictOptionsis the correct one for Angular + OpenIddict.AbpAccountOptions.IsTenantMultiDomain/GetTenantDomainare only read by the MVC Razor Pages account flow (and the old IdentityServer path), so they have no effect on a separated OpenIddict auth server. TheLinkLogin/SwitchTenanttoken grants readAbpAccountOpenIddictOptions, which is exactly why configuring that one works for you. It's the right, supported options class here — it's just missing from the docs, and we'll improve the docs for this option.Configure<AbpAccountOpenIddictOptions>(options => { options.IsTenantMultiDomain = true; options.GetTenantDomain = (httpContext, info) => Task.FromResult(/* tenant URL */); });2. The duplicate
tenant_domainis a bug, and removing the handler is the right workaround.When
IsTenantMultiDomainis on, the grant setstenant_domainon the sign-inAuthenticationProperties. OpenIddict already promotes that into the token response on its own, so the extraLinkLoginExtensionGrantProcessJsonResponsehandler adds the same key a second time, andAddParameterthrows on the duplicate — that's the HTTP 500. The handler is redundant, which is why removing it keepstenant_domainin the response and everything works.For the affected version, use this as a temporary workaround. If your version registers both handlers, remove both — they both hook the token response and key off the same
tenant_domainparameter, so leaving theSwitchTenantone in can still throw on aLinkLoginresponse:using OpenIddict.Server; using Volo.Abp.Account.Web.ExtensionGrants; PreConfigure<OpenIddictServerBuilder>(builder => { builder.RemoveEventHandler(LinkLoginExtensionGrantProcessJsonResponse.Descriptor); builder.RemoveEventHandler(SwitchTenantExtensionGrantExtensionGrantProcessJsonResponse.Descriptor); });We're removing these redundant handlers in the next release, so once you upgrade to a version that includes the fix you can drop the
PreConfigureblock.3. Passing the token in the URL is the built-in behavior.
For a cross-subdomain switch the source and target tenants are different origins, so
localStoragecan't be shared between them. The AngularLinkLoginHandlertransfers the token once through the redirect URL, the destination reads it intolocalStorage, then removes it from the visible URL and reloads. That's the framework's own handler, so there's nothing custom to maintain on your side.There isn't a drop-in "more secure" variant in the Angular template. The redirect still passes through the address bar / browser history / any proxy or log in between, so if your security requirements are stricter than that, the only real alternative is a different architecture — a shared cookie on the parent domain, or a backend-for-frontend that keeps the tokens server-side. For the built-in flow, keeping it on HTTPS across your own subdomains is the practical setup.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)