Open Closed

Cross-domain (tenant-per-subdomain) Linked-Account switch with Angular + OpenIddict #10756


User avatar
0
gouda created

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:

  1. LinkLoginExtensionGrant adds tenant_domain.
  2. 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

  1. Is AbpAccountOpenIddictOptions the correct and supported configuration for Angular + OpenIddict linked-account switching?
  2. Are the duplicate tenant_domain exception and the need to remove LinkLoginExtensionGrantProcessJsonResponse a known issue, or is there a recommended fix?
  3. 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?
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

1 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    1. AbpAccountOpenIddictOptions is the correct one for Angular + OpenIddict.

    AbpAccountOptions.IsTenantMultiDomain / GetTenantDomain are 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. The LinkLogin / SwitchTenant token grants read AbpAccountOpenIddictOptions, 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_domain is a bug, and removing the handler is the right workaround.

    When IsTenantMultiDomain is on, the grant sets tenant_domain on the sign-in AuthenticationProperties. OpenIddict already promotes that into the token response on its own, so the extra LinkLoginExtensionGrantProcessJsonResponse handler adds the same key a second time, and AddParameter throws on the duplicate — that's the HTTP 500. The handler is redundant, which is why removing it keeps tenant_domain in 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_domain parameter, so leaving the SwitchTenant one in can still throw on a LinkLogin response:

    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 PreConfigure block.

    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 localStorage can't be shared between them. The Angular LinkLoginHandler transfers the token once through the redirect URL, the destination reads it into localStorage, 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)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.