Open Closed

Domain/Subdomain Tenant Resolver not working #10222


User avatar
0
franciscosl created

Description

Hello ABP Team,

We are trying to implement tenant resolution by subdomain following the official ABP documentation, but the described approach does not work as expected, even in a clean ABP solution created from the templates.

We have followed the documentation step by step, specifically the section:

https://abp.io/docs/latest/framework/architecture/multi-tenancy#domainsubdomain-tenant-resolver

However, the tenant is not resolved correctly from the subdomain, and the application does not behave as expected.

Environment

  • ABP Framework: 9.1.3

  • UI: Blazor Interactive (WebAssembly only)

  • Multi-tenancy: enabled

  • Reproduced both in:

    • Existing project
    • Clean ABP solution generated from scratch

URL structure / scenario

Our application is deployed with environment-based subdomains:

  • QA: https://app-qa.example.com
  • Preproduction: https://app-pre.example.com
  • Production: https://app.example.com

Expected tenant URLs would be, for example:

  • https://tenant1.app-qa.example.com
  • https://tenant2.app-pre.example.com
  • https://tenant3.app.example.com

The base subdomain (app, app-qa, app-pre) represents the application, not a tenant.

What we have tried

1. Built-in Domain/Subdomain Tenant Resolver

  • Configured exactly as described in the documentation.
  • The tenant is not resolved, or the resolver is not triggered as expected.

2. Custom Tenant Resolver

  • Implemented a custom ITenantResolveContributor to handle our URL logic.
  • The resolver correctly extracts the tenant name from the URL/subdomain.
  • The extracted tenant value is available inside the resolver.

However:

  • Even though the tenant name is extracted correctly, the application does not work properly.
  • The tenant context does not seem to be applied correctly across the request lifecycle.
  • Authentication, login, and general request handling break or behave inconsistently.

This suggests the issue is not only related to parsing the URL, but to how the resolved tenant is applied internally by ABP, possibly in a Blazor Interactive WebAssembly scenario.

Expected behavior

  • The tenant should be resolved automatically from the subdomain.
  • The resolved tenant should be applied correctly to the request context.
  • The application should work normally once the tenant is resolved.
  • The login process should be able to use the resolved tenant.

Actual behavior

  • Built-in resolver does not work as documented.
  • Custom resolver extracts the tenant correctly, but the application does not function correctly.
  • Same behavior in a clean ABP 9.1.3 Blazor Interactive WebAssembly solution.

Questions

  1. Is the Domain/Subdomain Tenant Resolver fully supported in ABP 9.1.3 with Blazor Interactive (WebAssembly only)?

  2. Is there any additional configuration, middleware order, or Blazor-specific requirement missing from the documentation?

  3. Are there known limitations or issues when:

    • Using environment-based subdomains
    • Using Blazor Interactive WebAssembly
  4. Is there a recommended or official approach for resolving tenants by subdomain in this setup?

Any clarification, guidance, or updated example would be greatly appreciated.

Thank you in advance.


4 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution Below is a definitive checklist and working setup to make domain/subdomain tenant resolution work with ABP 9.1.x for Blazor Interactive (WebAssembly-only) and a separated Auth Server (default OpenIddict). It addresses both backend resolution and the Blazor WASM client side, including environment-based subdomains like app-qa.example.com, app-pre.example.com, app.example.com with tenant1.app-qa.example.com, etc.

    A) Backend (HttpApi.Host) – Resolve tenant from subdomain and allow wildcard OpenIddict/redirects

    1. Enable wildcard domain support for OpenIddict (required for subdomain tenants)
    • In YourHttpApiHostModule.PreConfigureServices:
    // using Volo.Abp.OpenIddict.WildcardDomains;
    PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
    {
        options.EnableWildcardDomainSupport = true;
        // Add all authority formats that will be used by the auth server (API host if combined)
        // Example for environments:
        options.WildcardDomainsFormat.Add("https://{0}.app-qa.example.com");
        options.WildcardDomainsFormat.Add("https://{0}.app-pre.example.com");
        options.WildcardDomainsFormat.Add("https://{0}.app.example.com");
        // If your API host is also subdomained (like {0}.api.app-qa.example.com), add those too.
    });
    
    1. Add Domain/Subdomain tenant resolver
    • In YourHttpApiHostModule.ConfigureServices:
    // using Volo.Abp.MultiTenancy;
    Configure<AbpTenantResolveOptions>(options =>
    {
        // Adjust formats to your real hostnames used for API endpoints receiving requests.
        options.AddDomainTenantResolver("{0}.app-qa.example.com");
        options.AddDomainTenantResolver("{0}.app-pre.example.com");
        options.AddDomainTenantResolver("{0}.app.example.com");
        // If your API host has a different subdomain pattern, add its formats as well.
    });
    
    1. If using a separated Auth Server (or validating tokens issued for wildcard domains)
    • Configure JWT bearer validation to accept wildcard issuers when you do token validation on gateway/API hosts that are not the issuer:
    // using Owl.TokenWildcardIssuerValidator;
    context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = configuration["AuthServer:Authority"]; // Your base authority
        options.RequireHttpsMetadata = true;
    
        // Allow wildcard issuers for subdomain tenants
        options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
        options.TokenValidationParameters.ValidIssuers = new[]
        {
            "https://{0}.app-qa.example.com/",
            "https://{0}.app-pre.example.com/",
            "https://{0}.app.example.com/"
        };
    });
    
    1. CORS and Redirect URIs for wildcard subdomains
    • Use ABP helpers that support wildcard subdomains for CORS and redirect URI validation:
    context.Services.AddAbpStrictRedirectUriValidator();
    context.Services.AddAbpClientConfigurationValidator();
    context.Services.AddAbpWildcardSubdomainCorsPolicyService();
    
    • In DbMigrator appsettings.json (Identity/OpenIddict client registrations), set RootUrl with {0} placeholder so redirect URIs and post-logout URIs are generated per-tenant:
    "IdentityServer": {
      "Clients": {
        "My_Blazor_Wasm": {
          "ClientId": "My_Blazor_Wasm",
          "ClientSecret": "1q2w3e*",
          "RootUrl": "https://{0}.app-qa.example.com"
        }
      }
    }
    

    Repeat entries for each environment if you keep per-env DBs; otherwise include each environment’s RootUrl pattern if you use one central auth DB for all envs. Re-run DbMigrator so allowed redirect URIs/CORS origins are updated.

    1. Issuer URI when using OpenIddict If you customized IssuerUri earlier, ensure it does not conflict with wildcard expectations. Commonly you don’t set a fixed IssuerUri when using wildcard support; instead rely on AbpOpenIddictWildcardDomainOptions above.

    B) Blazor Interactive (WebAssembly-only) – Propagate the current tenant’s subdomain to Authority and BaseUrl

    For Blazor WebAssembly, there is no server-side tenant resolver in the UI process. You must dynamically shape the Authority and BaseUrl at startup using the current browser URL. This is documented for Angular and demonstrated for Blazor in community samples. Implement the pattern below:

    1. In the Blazor WASM client’s Program.cs/Module.Initialize:
    • Read the current origin (builder.HostEnvironment.BaseAddress) and inject the tenant subdomain into the Authority and the RemoteServices BaseUrl by replacing {0}. The key is: never hardcode a single Authority; compute it based on current host.

    Example utility methods:

    private static readonly string[] ProtocolPrefixes = { "http://", "https://" };
    
    private static string ConvertToTenantSubDomain(WebAssemblyHostBuilder builder, string configPath)
    {
        var baseUrl = builder.HostEnvironment.BaseAddress; // e.g., https://tenant1.app-qa.example.com/
        var configUrl = builder.Configuration[configPath];  // e.g., "https://{0}.api.app-qa.example.com"
        return configUrl.Replace("{0}.", GetTenantNamePrefix(baseUrl));
    }
    
    // Returns "tenant1." or "" when no tenant
    private static string GetTenantNamePrefix(string baseUrl)
    {
        var hostName = baseUrl.RemovePreFix(ProtocolPrefixes); // tenant1.app-qa.example.com/
        var host = hostName.TrimEnd('/').Split('/')[0];        // tenant1.app-qa.example.com
        var parts = host.Split('.');
        // Your rule: "tenant-env.example.com" => first label is tenant, second is env-app
        // E.g., tenant1.app-qa.example.com => tenant is parts[0], rest identifies environment/app
        return parts.Length > 2 ? $"{parts[0]}." : string.Empty;
    }
    
    1. Apply tenantized Authority and BaseUrl
    • When configuring authentication:
    builder.Services.AddOidcAuthentication(options =>
    {
        builder.Configuration.Bind("AuthServer", options.ProviderOptions);
        options.UserOptions.RoleClaim = "role";
        options.ProviderOptions.Authority = ConvertToTenantSubDomain(builder, "AuthServer:Authority");
    });
    
    • When configuring remote services:
    Configure<AbpRemoteServiceOptions>(options =>
    {
        options.RemoteServices.Default = new RemoteServiceConfiguration(
            ConvertToTenantSubDomain(builder, "RemoteServices:Default:BaseUrl")
        );
    });
    
    1. appsettings.json (Blazor client) Provide patterns with {0} in the Blazor client so the above replacement works:
    {
      "App": {
        "SelfUrl": "https://{0}.app-qa.example.com"
      },
      "AuthServer": {
        "Authority": "https://{0}.api.app-qa.example.com",
        "ClientId": "My_Blazor_Wasm",
        "ResponseType": "code"
      },
      "RemoteServices": {
        "Default": {
          "BaseUrl": "https://{0}.api.app-qa.example.com"
        }
      }
    }
    

    Repeat per environment or compute environment from the current host if you prefer a single file with multiple candidates.

    C) Cookie/Authentication isolation across tenants in WASM If you observe “login to one tenant logs me into all,” your auth cookie is likely scoped to the parent domain. Always let the auth cookie domain be the exact host (no leading dot, Domain = null). OpenIddict server defaults typically align with this; avoid setting a shared cookie domain. Clear browser cookies after changes.

    D) Resolver order and middleware ABP’s default resolver order starts with CurrentUserTenantResolveContributor, then QueryString, Route, Header, Cookie. Adding the domain resolver (as shown) integrates correctly. Ensure app.UseAbpRequestLocalization(), app.UseRouting(), app.UseAuthentication(), app.UseAbpMultiTenancy(), app.UseAuthorization() are in the standard order generated by templates.

    E) Environment-based subdomains (app-qa/app-pre/app)

    • Add one resolver format per actual host pattern you expect to hit on the API/Auth side:
      • "{0}.app-qa.example.com"
      • "{0}.app-pre.example.com"
      • "{0}.app.example.com"
    • Make sure your DNS/ingress forwards those hosts to the API/Auth app.

    F) Known limitations and notes for Blazor WASM

    • There is no server-side Blazor UI pipeline to run a tenant resolver in WASM; you must shape the Authority and BaseUrl on the client at startup as above. This is the recommended approach for WASM.
    • For the backend OpenIddict/redirect handling with subdomains, wildcard configuration is required (as in Section A).

    G) Minimal verification steps

    1. Create a tenant named tenant1.
    2. Browse to https://tenant1.app-qa.example.com
    3. Observe the Blazor WASM client sets Authority/BaseUrl to https://tenant1.api.app-qa.example.com (or your pattern).
    4. Login succeeds; API requests include the correct tenant context, and ICurrentTenant is populated on the server.
    5. Browse in a separate tab to another tenant’s URL; it should remain isolated.
    1. Sources
    • https://abp.io/docs/latest/framework/architecture/multi-tenancy#domainsubdomain-tenant-resolver
    • https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver

    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.

  • User Avatar
    0
    franciscosl created

    Unfortunately, the proposed solution does not work for us. We have tested it both in our application and in a clean ABP application, and the behavior is the same.

    To make this reproducible, I created a clean ABP 9.1.3 solution (Blazor Interactive – WebAssembly only) and applied the suggested changes.

    Reproducible repository: 👉 https://github.com/franciscosl-fnet/abp-app-domain-based

    In this sample:

    • The tenant name is correctly extracted from the subdomain.
    • However, the tenant context is not applied correctly, and the application does not work as expected.
    • Same behavior in our real project and in this clean sample.

    This suggests the issue is not project-specific. Any guidance after reviewing the repo would be appreciated.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Reproducible repository: 👉 https://github.com/franciscosl-fnet/abp-app-domain-based

    Can you share the steps?

    I will check it

    Thanks.

  • User Avatar
    0
    franciscosl created

    [maliming] said: hi

    Reproducible repository: 👉 https://github.com/franciscosl-fnet/abp-app-domain-based

    Can you share the steps?

    I will check it

    Thanks.

    Hi, sorry, I thought that the problem description already explained how to reproduce it.

    1. Start the host (DbMigrator previously runned)
    2. Start Blazor
    3. Create a new tenant from the host
    4. Log out
    5. Use the URL format described in the ticket to access the newly created tenant (using the local URLs)
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.1.0-preview. Updated on December 16, 2025, 09:09
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.