Open Closed

Sub-Domain Tenant resolution in a Tiered Environment. Static Authserver domain (no sub-domain) #9504


User avatar
0
gthompson2005 created

I am certainly missing something, and or if my approach is non-standard, please advise and I'll adjust. For Management of the URL and SSL certificates I was attempting to use just a base auth.uat.domain.io address without creating a new cert to support wildcard *.auth.uat.domain.io.

UI: Blazor DB: EF Core Tiered Project **Objective: ** Deploy a Blazor App that resolves tenant based on subdomain in a UAT and a Production environment. Right now I just have the UAT environment which is reachable like uat.domain.io. If I visit the tenant it is tenant1.uat.domain.io I want the login page on authentication server to know that tenant1 is the Tenant. Additional details. Authentication server is hosted at auth.uat.domain.io API Server is hosted on api.uat.domain.io Blazor App Server is hosted on uat.domain.io with wildecards enabled What I expect to Happen: User visits tenant.uat.domain.io Application validates Tenant on Blazor app and directs to Account/Login on Auth Server Auth Server validates Tenant without using subdomain (limiting this to just point to auth.uat.domain.io)
If no Subdomain or tenant resolved assume host If tenant resolved on blazor side from subdomain, then use that tenant for logging in as well via querystring or other methods of sharing.

What is happening: When visiting the domain as uat.domain.io where the user has previously logged in (but logged out) The login page retains the previous tenant regardless of if visiting uat.domain.io or tenant2.uat.domain.io. If I start an incognito window and visit tenant1.uat.domain.io the tenant resolves to the host tenant regardless of subdomain.
I setup a custom Querystring tenant resolver on auth to resolve tenant from the URL (its embedded in the returnurl) and get the authserver to resolve my tenant but then it reverts to host as final

** Log information** 2025-06-22 00:33:39.670 +00:00 [INF] The authorization request was successfully extracted: { "client_id": "BlazorServer", "redirect_uri": "https://mytenant.uat.domain.io/signin-oidc", "response_type": "code id_token", "scope": "openid profile roles email phone ", "response_mode": "form_post", "nonce": "638861492195962570.YTM1NDAwNjctOTg2Ny00Njc0LTkxNTAtY2UwNzY5ZDdhOWY3MGU4NWU4ZGEtZGYxYi00NTEwLWJmYjctYWJkZmQ5YjE5ZDIy", "tenant": "mytenant", "state": "Q", "x-client-SKU": "ID_NET9_0", "x-client-ver": "8.3.0.0" }. 2025-06-22 00:33:39.701 +00:00 [INF] AuthServer OIDC: ValidateAuthorizationRequest for tenant: (host) 2025-06-22 00:33:39.702 +00:00 [INF] The authorization request was successfully validated.

Blazor Module Code Snippet Configure<AbpTenantResolveOptions>(options => { options.TenantResolvers.Clear(); options.TenantResolvers.Insert(0, new SubdomainTenantResolveContributor()); options.TenantResolvers.Add(new QueryStringTenantResolveContributor()); options.TenantResolvers.Add(new HeaderTenantResolveContributor()); options.TenantResolvers.Add(new CookieTenantResolveContributor()); });

public class SubdomainTenantResolveContributor : ITenantResolveContributor { public string Name => "Subdomain";

 private static readonly string[] NonTenantSubdomains = {
     "localhost", "www", "auth", "api", "uat"
 };

 public Task ResolveAsync(ITenantResolveContext context)
 {
     var httpContextAccessor = context.ServiceProvider.GetService&lt;IHttpContextAccessor&gt;();
     var logger = context.ServiceProvider.GetService&lt;ILogger&lt;SubdomainTenantResolveContributor&gt;>();

     var host = httpContextAccessor?.HttpContext?.Request?.Host.Host;
     string resolvedTenant = null;

     if (!string.IsNullOrWhiteSpace(host))
     {
         var parts = host.Split('.');
         var subdomain = parts.FirstOrDefault()?.ToLowerInvariant();

         // Exclude known non-tenant subdomains and azurewebsites
         if (!string.IsNullOrWhiteSpace(subdomain)
             && !NonTenantSubdomains.Contains(subdomain)
             && !host.Contains("azurewebsites.net"))
         {
             resolvedTenant = subdomain;
             context.TenantIdOrName = resolvedTenant;
         }

         logger?.LogInformation("SubdomainTenantResolveContributor - Host: {Host}, ResolvedTenant: {Tenant}", host, resolvedTenant ?? "(none)");
     }
     else
     {
         logger?.LogWarning("SubdomainTenantResolveContributor - No host available from HttpContext.");
     }

     return Task.CompletedTask;
 }

}

Auth Server Module: Configure<AbpTenantResolveOptions>(options => { options.TenantResolvers.Clear(); options.TenantResolvers.Insert(0, new ReturnUrlQueryTenantResolveContributor()); options.TenantResolvers.Insert(1, new CookieTenantResolveContributor()); options.TenantResolvers.Add(new QueryStringTenantResolveContributor()); options.TenantResolvers.Add(new HeaderTenantResolveContributor());

    });

public class ReturnUrlQueryTenantResolveContributor : ITenantResolveContributor { public string Name => "ReturnUrlQuery";

public async Task ResolveAsync(ITenantResolveContext context)
{
    var serviceProvider = context.ServiceProvider;
    var httpContextAccessor = serviceProvider.GetService(typeof(IHttpContextAccessor)) as IHttpContextAccessor;
    var logger = serviceProvider.GetService(typeof(ILogger&lt;ReturnUrlQueryTenantResolveContributor&gt;)) as ILogger;

    var request = httpContextAccessor?.HttpContext?.Request;
    //var response = httpContextAccessor?.HttpContext?.Response;
    if (request == null)
    {
        logger?.LogWarning("No HttpContext found.");
        return;
    }

    var returnUrl = request.Query["ReturnUrl"].FirstOrDefault();
    if (string.IsNullOrWhiteSpace(returnUrl))
    {
        logger?.LogDebug("ReturnUrl missing or empty.");
        return;
    }

    try
    {
        var decodedUrl = Uri.UnescapeDataString(returnUrl);
        var queryIndex = decodedUrl.IndexOf('?');
        if (queryIndex >= 0)
        {
            var queryString = decodedUrl.Substring(queryIndex);
            var parsedQuery = QueryHelpers.ParseQuery(queryString);
            if (parsedQuery.TryGetValue("tenant", out StringValues tenantValues))
            {
                var tenant = tenantValues.FirstOrDefault();
                if (!string.IsNullOrWhiteSpace(tenant))
                {
                    context.TenantIdOrName = tenant;
                    context.Handled = true;

                    //if (response == null)
                    //{
                    //    logger?.LogWarning("No HttpContext Response found.");
                    //    return;
                    //}

                    //response.Cookies.Append("Abp.TenantId", tenant, new CookieOptions
                    //{
                    //    HttpOnly = true,
                    //    Secure = request.IsHttps,
                    //    SameSite = SameSiteMode.Lax,
                    //    Path = "/"
                    //});
                    logger?.LogInformation("Tenant resolved from ReturnUrl: {Tenant}", tenant);
                }
            }
        }
    }
    catch (Exception ex)
    {
        logger?.LogError(ex, "Error parsing tenant from ReturnUrl.");
    }

    await Task.CompletedTask;
}

}


3 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent

    To achieve subdomain-based tenant resolution in a tiered ABP Blazor Server environment with a static AuthServer domain (no wildcard), you need to ensure that the tenant context is consistently and securely passed from the Blazor app (which uses subdomains) to the AuthServer (which does not use subdomains). Your approach is mostly correct, but there are some ABP-recommended practices and built-in features that can simplify and standardize this process.

    Solution

    1. Blazor App: Subdomain Tenant Resolution

    • Use the built-in domain/subdomain tenant resolver:
      Configure<AbpTenantResolveOptions>(options =>
      {
          options.AddDomainTenantResolver("{0}.uat.domain.io");
      });
      
    • This will resolve the tenant from the subdomain (e.g., tenant1.uat.domain.io → tenant1).

    2. AuthServer: Tenant Resolution via Query String

    Since your AuthServer is at a static domain (auth.uat.domain.io), you cannot resolve the tenant from the subdomain. The recommended approach is to pass the tenant name as a query string parameter (e.g., ?tenant=tenant1) when redirecting to the AuthServer.

    • In your Blazor app, when redirecting to the AuthServer for login, append the tenant name to the return URL:

      var tenantName = currentTenant.Name;
      var returnUrl = $"/signin-oidc?tenant={tenantName}";
      // Use this returnUrl in your OIDC configuration
      
    • On the AuthServer, use the built-in QueryStringTenantResolveContributor:

      Configure<AbpTenantResolveOptions>(options =>
      {
          options.TenantResolvers.Clear();
          options.TenantResolvers.Add(new QueryStringTenantResolveContributor());
          options.TenantResolvers.Add(new CookieTenantResolveContributor());
          options.TenantResolvers.Add(new HeaderTenantResolveContributor());
      });
      

      This will resolve the tenant from the tenant query string parameter.

    3. Ensure Consistent Tenant Context

    • Always include the tenant name in the authentication flow (as a query string) when redirecting between the Blazor app and the AuthServer.
    • If you use ABP's built-in OIDC integration, you can override the authority and redirect URIs to include the tenant parameter.

    4. CORS and Redirect URIs

    • In your IdentityServer/DbMigrator configuration, set the client RootUrl and allowed CORS origins to use the {0} placeholder:
      "RootUrl": "https://{0}.uat.domain.io"
      
    • This allows any tenant subdomain to be accepted.

    5. Example: ABP Sample

    ABP provides a sample for subdomain tenant resolution:

    • https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver

    6. Important Notes

    • Do not clear and re-order the default tenant resolvers unless you have a specific reason. Use ABP's built-in contributors where possible.
    • The tenant context must be explicitly passed from the UI to the AuthServer if the AuthServer does not use subdomains.
    • If you want to support both host and tenant logins, ensure your logic falls back to host if no tenant is resolved.

    References

    This approach is standard for ABP multi-tenancy in tiered environments with a static AuthServer domain.


    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
    gthompson2005 created

    My custom subdomain resolver excludes various subdomains such as uat, api, auth which are used for the various services as well as the uat base in this case is for the host.

    I have previously added the following to AddAbpOpenIdConnect options

    options.Events.OnRedirectToIdentityProvider = context =>
    {
        var httpContext = context.HttpContext;
        var currentTenant = httpContext.RequestServices
            .GetRequiredService<ICurrentTenant>();
    
        if (currentTenant?.Name != null)
        {
            context.ProtocolMessage.SetParameter("tenant", currentTenant.Name);
        }
        else
        {
            // On host tenant access, ensure we don't leak the last tenant
            context.ProtocolMessage.Parameters.Remove("tenant");
            context.HttpContext.Response.Cookies.Delete("Abp.TenantId");
        }
    
        return Task.CompletedTask;
    };
    
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    I guess your blazor app is Blazor Server.

    1. You should add CurrentUserTenantResolveContributor in all projects.

    options.TenantResolvers.Insert(0, new CurrentUserTenantResolveContributor());

    2.

    What I expect to Happen: User visits tenant.uat.domain.io Application validates Tenant on Blazor app and directs to Account/Login on Auth Server Auth Server validates Tenant without using subdomain (limiting this to just point to auth.uat.domain.io) If no Subdomain or tenant resolved assume host If tenant resolved on blazor side from subdomain, then use that tenant for logging in as well via querystring or other methods of sharing.

    2.1 Does your tenant work on a Blazor project?

    2.2

    directs to Account/Login on Auth Server

    Auth Server can't know your current tenant of Blazor, which means you need to pass tenant info to Auth Server. eg: auth.uat.domain.io?__tenant=tenant1

    1. Once you log in to the Auth Server. The current tenant of AuthServer will not change. because of CurrentUserTenantResolveContributor

    Thanks.

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.0.0-preview. Updated on July 09, 2025, 06:20