Target Goal: tenant1.domain.com -> webhost tenant2.domain.com -> webhost auth.domain.com -> auth server
When a user goes to login to tenant1.domain.com they will be logged into tenant1 after they successfully entered credentials in auth
What is happening: When we login we are encountering 2 different issues. One being we get stuck in a redirect loop and the other is we are just logged in to the host admin, which I assume is because there is no tenant being resolved.
What has been tried: We followed the steps here : https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver We added it to just the auth server with no success and then to both the auth server and web host
11 Answer(s)
-
0
- Solution To make domain/subdomain tenant resolution work when your Angular SPA, Auth Server, and API are on different domains, configure tenant resolution on both ends and make OIDC settings domain-aware. Use the following checklist and code/config exactly.
A) Backend (Auth Server and Web Host/API)
- Enable domain tenant resolver on every HTTP entry point that must infer tenant from host (at least the Web Host/API, and usually the Auth Server if you want tenant-aware login UI without switching).
using Volo.Abp.MultiTenancy; public override void ConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); // Strongly recommended for wildcard tenants context.Services.AddAbpStrictRedirectUriValidator(); context.Services.AddAbpClientConfigurationValidator(); context.Services.AddAbpWildcardSubdomainCorsPolicyService(); Configure<AbpTenantResolveOptions>(options => { // For Web Host/API if tenants are like tenant1.domain.com calling the API at api.domain.com // If your API domain is api.domain.com and NOT tenant-qualified, do NOT add tenant resolver here. // If your API domain is tenant-qualified (tenant1.api.domain.com), add it: // options.AddDomainTenantResolver("{0}.api.domain.com"); // For Auth Server if you want tenant-qualified issuer/login page (only when using per-tenant auth host, e.g. tenant1.auth.domain.com) // options.AddDomainTenantResolver("{0}.auth.domain.com"); }); }If you use a single shared Auth Server domain (auth.domain.com) for all tenants:
- Do NOT add domain tenant resolver for Auth Server. Instead, pass the tenant explicitly via __tenant on the authorize request (see Frontend section).
- Ensure OpenIddict/IdentityServer has per-tenant redirect URIs and CORS origins registered for each SPA domain.
If you use per-tenant Auth Server subdomains (tenant1.auth.domain.com):
- Enable wildcard domain support for OpenIddict issuer and token validation.
- Configure wildcard domain formats:
using Volo.Abp.OpenIddict.WildcardDomains; public override void PreConfigureServices(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); PreConfigure<AbpOpenIddictWildcardDomainOptions>(options => { options.EnableWildcardDomainSupport = true; // Use your actual format options.WildcardDomainsFormat.Add("https://{0}.auth.domain.com"); // Optionally also add SPA and API formats if needed by your setup: // options.WildcardDomainsFormat.Add("https://{0}.domain.com"); // options.WildcardDomainsFormat.Add("https://{0}.api.domain.com"); }); }- Token issuer validation (only if you use per-tenant issuers or wildcard issuers and face issuer mismatch):
- If necessary, add Owl.TokenWildcardIssuerValidator to accept wildcard issuers:
// In API/Web Host JWT bearer configuration services.AddAuthentication().AddJwtBearer(options => { options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator; options.TokenValidationParameters.ValidIssuers = new[] { "https://{0}.auth.domain.com/" }; });This is only needed if your API validates tokens issued by many per-tenant issuer URLs.
- Configure App URLs and Redirect URIs
- In DbMigrator appsettings (IdentityServer or OpenIddict clients), set SPA client RootUrl with {0} placeholder and then run DbMigrator so the client config is updated in DB:
{ "IdentityServer": { "Clients": { "MyApp_App": { "ClientId": "MyApp_App", "ClientSecret": "1q2w3e*", "RootUrl": "https://{0}.domain.com" } } } }- Do not store wildcards like https://*.domain.com in the database; use concrete origins or the {0} pattern managed by ABP’s DbMigrator. For a single shared Auth Server (auth.domain.com), the issuer stays constant; the redirect URIs list must include each SPA domain (DbMigrator {0} pattern generates entries per tenant).
B) Frontend (Angular)
- Use {0} in baseUrl and redirectUri so ABP Angular determines tenant from subdomain and hides the tenant switcher:
const baseUrl = 'https://{0}.domain.com'; export const environment = { production: true, application: { baseUrl, name: 'MyApp', }, oAuthConfig: { issuer: 'https://auth.domain.com', // Shared Auth Server domain redirectUri: baseUrl, // per-tenant SPA domain clientId: 'MyApp_App', responseType: 'code', scope: 'offline_access MyApp', requireHttps: true }, apis: { default: { url: 'https://api.domain.com', // shared API domain or tenant-qualified if you use that model rootNamespace: 'MyApp' } } };- Pass the tenant on login when using a shared Auth Server domain. This prevents the login page from offering tenant switching and ensures proper tenant context:
import { AuthService, SessionStateService } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; @Component({ selector: 'app-home', templateUrl: './home.component.html' }) export class HomeComponent { private authService = inject(AuthService); private session = inject(SessionStateService); login() { const tenant = this.session.getTenant(); // Prefer tenant id if available; __tenant accepts id or name const tenantParam = tenant?.id?.toString() ?? tenant?.name ?? null; if (tenantParam) { this.authService.navigateToLogin({ __tenant: tenantParam }); } else { this.authService.navigateToLogin(); } } }This is the recommended approach when your Auth Server is not tenant-qualified (auth.domain.com).
- Ensure the Angular app does not auto-trigger login on initial navigation before the tenant is resolved (avoid redirect loops). Make HomeComponent public and let the user click Login or trigger login after session is initialized.
C) Common causes of redirect loops or logging into host
- Missing or wrong Redirect URIs/CORS: Each tenant SPA domain must be registered on the Auth Server client (DbMigrator with RootUrl using {0} and rerun after adding a new tenant).
- SPA uses a protected route immediately on app start, causing repeated challenges. Make the first page public.
- Not passing __tenant to the Auth Server when using a shared Auth Server domain.
- Issuer mismatch in API when using per-tenant issuer domains; fix with wildcard issuer support or standardize on a single issuer domain.
D) Which topology is supported?
- Option 1 (Preferred for simplicity): One shared Auth Server (auth.domain.com) and one shared API (api.domain.com), many tenant SPAs on subdomains ({0}.domain.com). Pass __tenant during login. This is supported and commonly recommended.
- Option 2: Per-tenant Auth Server/API domains ({0}-auth.domain.com, {0}-api.domain.com, {0}-app.domain.com). Also supported but requires wildcard issuer support and more complex configuration.
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/framework/ui/angular/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.
-
0
We tried this solution but our application still isnt picking this up and working. One thing that we noticed was that our route was defaulting to dashboard
const routes: Routes = [ { path: '', pathMatch: 'full', redirectTo: 'dashboard', },So we are unsure where to add the code where we pass the tenet id before they are getting redirected to the auth server. Also another modification to the original question:
Target Goal: tenant1.domain.com -> webhost (same azure webapp just different domain name) tenant2.domain.com -> webhost (same azure webapp just different domain name) azurehosted-domain.com -> auth server
-
0
- Solution Given your topology:
- tenant1.domain.com and tenant2.domain.com -> Angular SPA (same Azure web app, different custom domains)
- azurehosted-domain.com -> Auth Server (shared for all tenants)
Configure the flow so Angular determines the tenant from subdomain, delays auto-login until tenant is resolved, and passes the tenant to the Auth Server on navigation.
A) Angular: make the first route public and pass __tenant on login
- Keep the default redirect to dashboard only after authentication. Ensure the initial route is public and does not trigger guarded navigation on app start (this prevents redirect loops).
- Example routing (home public, dashboard protected by auth guard):
const routes: Routes = [ { path: '', component: HomeComponent }, // public entry, no auth guard { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: '**', redirectTo: '' } ];- In HomeComponent, call navigateToLogin with the tenant. Get tenant from SessionStateService (ABP Angular resolves tenant from subdomain automatically when you use {0} in baseUrl):
import { AuthService, SessionStateService } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-home', templateUrl: './home.component.html' }) export class HomeComponent { private auth = inject(AuthService); private session = inject(SessionStateService); private router = inject(Router); get hasLoggedIn() { return this.auth.isAuthenticated; } ngOnInit() { // If already logged in, go to dashboard; otherwise show a Login button in the template if (this.hasLoggedIn) { this.router.navigate(['/dashboard']); } } login() { const t = this.session.getTenant(); const tenantParam = t?.id?.toString() ?? t?.name ?? null; // Pass __tenant only when using a shared auth domain tenantParam ? this.auth.navigateToLogin({ __tenant: tenantParam }) : this.auth.navigateToLogin(); } }- Environment config: use {0} for baseUrl and redirectUri so the SPA infers tenant from subdomain and hides tenant switcher. Keep issuer pointing to your shared auth server domain.
const baseUrl = 'https://{0}.domain.com'; export const environment = { production: true, application: { baseUrl, name: 'MyApp' }, oAuthConfig: { issuer: 'https://azurehosted-domain.com', // shared Auth Server redirectUri: baseUrl, // per-tenant SPA domain clientId: 'MyApp_App', responseType: 'code', scope: 'offline_access MyApp', requireHttps: true }, apis: { default: { url: 'https://api.domain.com', rootNamespace: 'MyApp' } } };Notes:
- Do not auto-call login in constructor/ngOnInit before SessionStateService is ready. Use a public landing page and a Login button (or trigger login after ngOnInit once tenant is available). This avoids the redirect loop you observed when redirecting to dashboard immediately.
B) Auth Server: shared domain, no domain resolver, allow per-tenant redirect URIs
- Do not add a domain tenant resolver for the Auth Server since it uses a single domain (azurehosted-domain.com). The tenant is provided via __tenant by the SPA.
- Ensure the OIDC client has RedirectUris and AllowedCorsOrigins for each tenant SPA domain. Use DbMigrator with RootUrl = https://{0}.domain.com so new tenants are registered automatically: appsettings for DbMigrator:
{ "IdentityServer": { "Clients": { "MyApp_App": { "ClientId": "MyApp_App", "ClientSecret": "1q2w3e*", "RootUrl": "https://{0}.domain.com" } } } }- Run DbMigrator after adding tenants so the Auth Server database contains concrete redirect URIs and CORS origins for tenant1.domain.com, tenant2.domain.com, etc. Do not use wildcards like https://*.domain.com in redirect/CORS.
C) API/Web Host: domain tenant resolver if API is tenant-qualified; otherwise none
- If your API is a single shared host like api.domain.com (not tenant subdomains), you typically do NOT add the domain tenant resolver there. The SPA sends the __tenant header automatically and ABP resolves it via the header contributor.
- If your API is also tenant-qualified (e.g., tenant1.api.domain.com), add:
Configure<AbpTenantResolveOptions>(options => { options.AddDomainTenantResolver("{0}.api.domain.com"); });D) Fix the redirect loop and “logging into host” symptoms
- Loop: happens when the first route is protected and the app triggers login before tenant is resolved. Make the first route public and initiate login only after SessionStateService has the tenant, using navigateToLogin({ __tenant }).
- Logged into host: happens when Auth Server didn’t get a tenant. Ensure navigateToLogin includes __tenant and the SPA environment uses {0} in baseUrl/redirectUri so Angular resolves and sets tenant context.
E) Summary of what to change in your case
- Keep your current redirectTo('dashboard') only if the default ('') route component is public and checks hasLoggedIn first; otherwise remove the auto-redirect and present a Login button.
- Implement HomeComponent.login() as shown to pass __tenant.
- Ensure DbMigrator’s RootUrl uses {0} and rerun it after tenants are created.
- Do not configure domain tenant resolver on the Auth Server, since it’s azurehosted-domain.com shared for all tenants.
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver
- https://abp.io/docs/latest/framework/ui/angular/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.
-
0
We do not want to present a login button or access the home component. The authserver has the login form. We just need to pass the tenant from the angular side to the auth server when the authguard redirects a non-authenticated user.
-
0
- Solution You can keep using the built-in AuthGuard redirect. Just ensure that when the guard triggers the OpenID Connect authorization request, the tenant identifier is appended as __tenant. Do it without adding a landing page or a button by customizing ABP Angular’s auth code flow to always include the tenant parameter taken from the subdomain.
A) Angular: force AuthGuard -> authorize request to include __tenant
- ABP Angular’s AuthService.navigateToLogin accepts extra query parameters (including __tenant). Hook into login redirection globally so every guard-triggered login carries the tenant.
- Provide a custom implementation that wraps the default AuthCodeFlowStrategy (or AuthService) and inject it via Angular DI.
Example: override AuthService.navigateToLogin to append __tenant automatically:
import { Injectable, inject } from '@angular/core'; import { AuthService, SessionStateService } from '@abp/ng.core'; @Injectable({ providedIn: 'root' }) export class TenantAwareAuthService { private auth = inject(AuthService); private session = inject(SessionStateService); navigateToLogin(extras?: Record<string, unknown>) { const t = this.session.getTenant(); const tenantParam = t?.id?.toString() ?? t?.name ?? null; const qp = { ...(extras ?? {}) }; if (tenantParam && !qp['__tenant']) { qp['__tenant'] = tenantParam; } this.auth.navigateToLogin(qp); } }Then register a provider so guards use your service:
- If you call AuthService directly in guards or interceptors, replace usages with TenantAwareAuthService.
- If you rely on built-in ABP auth redirection, provide a global redirection handler to call TenantAwareAuthService.navigateToLogin. For example, in your app.module:
import { APP_INITIALIZER, inject } from '@angular/core'; import { OAuthService } from '@abp/ng.oauth'; import { Router } from '@angular/router'; import { TenantAwareAuthService } from './tenant-aware-auth.service'; export function patchOAuthLoginRedirect() { return () => { const oAuth = inject(OAuthService) as any; const tenantAuth = inject(TenantAwareAuthService); // Patch startLogin to ensure __tenant is sent for any login attempt const original = oAuth.startLogin?.bind(oAuth) ?? null; if (original) { oAuth.startLogin = (options?: any) => { tenantAuth.navigateToLogin(options?.params); }; } }; } @NgModule({ // ... providers: [ { provide: APP_INITIALIZER, useFactory: patchOAuthLoginRedirect, multi: true }, ], }) export class AppModule {}Notes:
- The essence is: before any redirect to the Auth Server, call navigateToLogin with { __tenant: <id or name> }. The SessionStateService resolves tenant from the subdomain when you use {0} in the environment baseUrl/redirectUri.
- Keep your existing route guard on dashboard. The guard will trigger login, which now carries __tenant automatically.
B) Environment configuration
- Ensure tenant is resolvable from subdomain and that the SPA returns to the same subdomain:
const baseUrl = 'https://{0}.domain.com'; // same Azure app, custom domains per tenant export const environment = { production: true, application: { baseUrl, name: 'MyApp' }, oAuthConfig: { issuer: 'https://azurehosted-domain.com', // shared Auth Server redirectUri: baseUrl, // per-tenant SPA domain clientId: 'MyApp_App', responseType: 'code', scope: 'offline_access MyApp', requireHttps: true }, apis: { default: { url: 'https://api.domain.com', rootNamespace: 'MyApp' } } };C) Auth Server configuration (shared domain)
- Do not use domain tenant resolver on the Auth Server since it’s a single shared host; it will receive __tenant from the Angular app.
- Ensure each tenant SPA domain is registered in the client RedirectUris and CORS. Use DbMigrator with RootUrl = https://{0}.domain.com and rerun after creating tenants so it generates concrete entries for tenant1.domain.com, tenant2.domain.com, etc. Example (DbMigrator appsettings):
{ "IdentityServer": { "Clients": { "MyApp_App": { "ClientId": "MyApp_App", "ClientSecret": "1q2w3e*", "RootUrl": "https://{0}.domain.com" } } } }D) API/Web Host
- If your API is a single shared host (e.g., api.domain.com), you don’t need domain tenant resolver on the API. Angular will send the __tenant header on API calls automatically after login.
- If you have tenant-qualified API domains, add options.AddDomainTenantResolver("{0}.api.domain.com") accordingly.
E) Result
- A non-authenticated user hits a guarded route such as /dashboard.
- The guard triggers the login redirect; your TenantAwareAuthService appends __tenant based on the subdomain-derived SessionStateService.
- Auth Server receives an authorize request with __tenant, shows the tenant-fixed login without switching, and issues tokens for that tenant.
- Angular finishes code flow and lands on the same subdomain (redirectUri = {0}.domain.com), now fully tenant-scoped.
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver
- https://abp.io/docs/latest/framework/ui/angular/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.
-
0
When this was implemented, the redirect failed because it added the tenant query string in the wrong location: https://xxxxxxxx.azurewebsites.net/?__tenant=portal-dev/.well-known/openid-configuration
It causes the page to break because it should be after the .well-known/openid-configuration
-
0
- Solution The issue happens because __tenant is being appended to the issuer discovery URL instead of the authorize request URL. In ABP Angular, you must pass __tenant only when starting the authorization request (authorize), not when fetching discovery (/.well-known/openid-configuration).
Apply all of the following:
A) Keep issuer clean in Angular
- Ensure environment oAuthConfig.issuer is exactly your auth server base URL without any path or query:
oAuthConfig: { issuer: 'https://xxxxxxxx.azurewebsites.net', // NO trailing path, NO query redirectUri: 'https://{0}.your-domain.com', clientId: 'YourClientId', responseType: 'code', scope: 'offline_access YourScope', requireHttps: true }If issuer already includes /.well-known/openid-configuration or includes a query, Angular’s OIDC discovery will break.
B) Pass __tenant only to the authorize request
- Do not hook or modify discovery calls.
- Ensure your call to navigateToLogin adds __tenant via the extras parameter. This is forwarded only to the authorize endpoint by ABP’s AuthCodeFlowStrategy.
import { AuthService, SessionStateService } from '@abp/ng.core'; import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class TenantAwareLogin { private auth = inject(AuthService); private session = inject(SessionStateService); start() { const t = this.session.getTenant(); const tenantParam = t?.id?.toString() ?? t?.name ?? null; if (tenantParam) { this.auth.navigateToLogin({ __tenant: tenantParam }); } else { this.auth.navigateToLogin(); } } }- If you rely on guard-triggered redirects (no landing page), call the same navigateToLogin with extras from your custom guard or a global hook that replaces only the login redirection. Do not change the issuer URL or intercept /.well-known requests.
C) Avoid patching OAuth discovery If you patched OAuthService.startLogin or globally appended query params to all OAuth URLs, revert that. Only pass __tenant through AuthService.navigateToLogin extras so it reaches authorize, not discovery.
D) Auth Server (shared domain)
- Do not configure domain tenant resolver on the Auth Server for this topology.
- Ensure each SPA tenant domain is registered as redirect URI and CORS origin (use DbMigrator RootUrl = https://{0}.your-domain.com and rerun after adding tenants).
E) Quick checklist to eliminate the bad URL
- issuer equals https://xxxxxxxx.azurewebsites.net
- No code appends __tenant to issuer or discovery URL
- __tenant is only passed in navigateToLogin extras
- After change, the browser sequence should be:
- GET https://xxxxxxxx.azurewebsites.net/.well-known/openid-configuration (no __tenant here)
- 302 to https://xxxxxxxx.azurewebsites.net/connect/authorize?...&__tenant=portal-dev (tenant appears here)
- Sources
- https://abp.io/docs/latest/framework/architecture/multi-tenancy
- https://abp.io/docs/latest/framework/architecture/multi-tenancy#domain-subdomain-tenant-resolver
- https://abp.io/docs/latest/framework/ui/angular/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.
-
0
-
0
hi
When we login we are encountering 2 different issues. One being we get stuck in a redirect loop and the other is we are just logged in to the host admin, which I assume is because there is no tenant being resolved.
Can you share an online URL to reproduce this problem?
liming.ma@volosoft.com
Thanks.
-
0
Maliming,
Here is the latest on where we are and what we are trying to accomplish
site url: https://portal-dev.hiredhandsoftware.com/ auth server url: https://app-auth-dev-aims.azurewebsites.net/Account/Login
Expectation: When visiting this website you are redirected to the auth server with the tenant of "portal-dev". Once logged in you will be logged in with the tenant "portal-dev"
What we arent seeing: When redirected to the auth server we are not seeing the tenant information passed to the auth server
Here are the code changes we have made: Added a new authguard with redirect with the tenant information

-
0
hi
You have set the domain as the tenant resolver.
options.AddDomainTenantResolver("{0}.hiredhandsoftware.com");The results:
Site url:
https://portal-dev.hiredhandsoftware.com/, the tenant name is:portal-devAuth server url:https://app-auth-dev-aims.azurewebsites.net/Account/Login, the tenant name always behost, so you can't seethe tenant information passed to the auth serverYou can remove the
AddDomainTenantResolverfrom the Auth server, and add a__tenantparameter when redirecting to the Auth server from the site.https://app-auth-dev-aims.azurewebsites.net/Account/Login?__tenant=test&ReturnUrl=%2Fcon.....
Thanks.


