The reason you still see ProjectName is that it is coming from localization, not from getEnvConfig or any string in your source code.
In LeptonX Angular, the brand text in the left menu uses a localization key (in the theme library).
If the key is not defined in your app’s localization, ABP shows the key itself.
So if the theme uses "ProjectName" and there is no translation, the UI displays exactly ProjectName.
Since you couldn’t find "ProjectName" in your solution, it is almost certainly defined in the theme’s template (node_modules), and you just need to provide a translation for that key.
Do this:
Edit your Angular localization JSON, for example:
angular/src/assets/abp/en.json:
{
// ... existing keys
"ProjectName": "Brand Encoder Dev"
}
If you have other languages (e.g. ar.json, fr.json, …) add the same key there too:
{
"ProjectName": "Brand Encoder Dev"
}
You don’t have to find an existing
ProjectNamekey.
Just add it – ABP will merge it with the built‑in resources and the theme’s{{ 'ProjectName' | abpLocalization }}will start using your value.
For local:
cd angular
npm install # or yarn
npm run start
For AKS / production:
cd angular
npm run build -- --configuration production
# deploy the new dist/ to AKS
Then hard‑refresh the browser (Ctrl+F5).
After this, the highlighted text that currently shows ProjectName will display Brand Encoder Dev (or whatever you set in the localization file), independent of what getEnvConfig returns.
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.
The text you see (ProjectName) is not taken from environment.application.name.
It is a localization key rendered like this in the layout:
{{ 'ProjectName' | abpLocalization }}
Since there is a localization entry "ProjectName": "ProjectName", the UI shows exactly that.
To change it, update your Angular localization files:
Open the English localization file
In your Angular project:
angular/
src/
assets/
abp/
en.json
Change the ProjectName value
Find the ProjectName entry, which currently is something like:
{
"ProjectName": "ProjectName",
...
}
Change it to your desired name:
{
"ProjectName": "Brand Encoder Dev",
...
}
If you support other languages (e.g., tr.json, ar.json, …) update their ProjectName values as well.
# from angular/ folder
npm install # or yarn
npm run start # for local
# or
npm run build -- --configuration production # for AKS/deployment
Deploy the new build to AKS, then hard‑refresh the browser (Ctrl+F5).
After this, the sidebar/header text where you currently see ProjectName will show Brand Encoder Dev (or whatever value you put in the localization file), while getEnvConfig will continue to reflect your environment settings correctly.
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.
In ABP Angular + LeptonX, the text shown in the header/sidebar is taken from the Angular environment configuration, not from the backend. If it keeps showing ProjectName, it means the application.name field in your Angular environments still has the default value or the deployed build is not using the environment file you updated.
Do the following:
angular/src/environments/environment.ts:export const environment = {
production: false,
application: {
name: 'Brand Encoder', // <- your project name here
logoUrl: '/assets/images/logo.png', // optional
},
// ... the rest is unchanged
};
production: true, so you must also update:// angular/src/environments/environment.prod.ts
export const environment = {
production: true,
application: {
name: 'Brand Encoder', // <- same name or any text you want
logoUrl: '/assets/images/logo.png', // optional
},
// ... other config
};
Rebuild & redeploy the Angular app
From the angular folder:
# if you use yarn
yarn
yarn build --configuration production
# or with npm
npm install
npm run build -- --configuration production
Then redeploy the new dist output to your server (however you are currently publishing the Angular app). After deployment, clear the browser cache/hard reload.
Verify
Brand Encoder (or whatever you configured).ProjectName, double‑check you actually deployed the new dist files and that you edited the same environment files used by the build configuration you are running.Sources
application.name and logoUrl in environment.ts:application section in environment config: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.
ABP Studio always generates the latest stable ABP version it supports (and the corresponding .NET version). That’s why, if your Studio is up‑to‑date and the latest platform version is 10.x, new solutions will target ABP 10 / .NET 10, even if your existing services are on 9.2.3.
To generate a microservice template on ABP 9.2.3 with .NET 9, use the ABP CLI instead of Studio:
dotnet tool install -g Volo.Abp.Studio.Cli
# or, if already installed:
dotnet tool update -g Volo.Abp.Studio.Cli
abp new MyProject \
-t microservice-pro \
-v 9.2.3
Or (non‑Pro template):
abp new MyProject \
-t microservice \
-v 9.2.3
The -v / --version flag is the key part – it forces the solution to be created with that exact ABP version (thus using .NET 9 for ABP 9.x).
appsettings.json and launchSettings.json changes you already made.You can still afterwards open this CLI-generated solution in ABP Studio (for Solution Runner, health checks, etc.); Studio will work fine with a solution created via CLI at 9.2.3 – you just shouldn’t ask Studio to “upgrade/switch to stable 10.x” if you want to stay on 9.2.3.
If you later decide to move everything to ABP 10 / .NET 10, you can then use abp update or Studio’s upgrade features across the whole solution at once.
--version switch: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.
For long‑term maintenance and upgrades, it is better to stay on LeptonX and customize it, rather than going back to the old Lepton theme.
Reasons to prefer LeptonX:
MainLayout.razor and related components) into your solution, andLeptonXToolbars.Main) into a top header area while keeping the navigation menu on the left.When would going back to Lepton make sense?
Recommended approach
MainLayout.razor (and any related layout components) into your app.This way you get the UX your users want plus the benefits of the newer, actively enhanced LeptonX theme.
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.
From your screenshots and description, there are two separate but related problems after the ABP Studio update:
DbMigrator fails when creating a new solution.A very similar case was reported and solved; the root cause was a broken/global NuGet configuration on the machine, not ABP Studio itself.
ABP Studio (and its internal tools like Volo.Abp.Studio.Cli, DbMigrator, ABP Suite) are installed and restored via dotnet / NuGet. If your global NuGet.config points to an unreachable or invalid feed, these tools can’t be installed/restored, and you see generic “Couldn’t open ABP Suite” / “Job 'DbMigrator' FAILED” messages.
Follow these steps:
Locate your global NuGet.config as explained in the official NuGet docs (usually under:
%AppData%\NuGet\NuGet.config~/.config/NuGet/NuGet.config or ~/.nuget/NuGet/NuGet.Config)Open the file and look for <packageSources> section.
If you see company‑internal or unreachable feeds (for example a URL that’s only valid on VPN or no longer exists), either:
<add key="..." value="https://your-company-feed/..."/> line, orprotocolVersion="3" and ensure the URL is correct and reachable, orenabled="false" (on recent NuGet versions).Ensure that at least https://api.nuget.org/v3/index.json exists and is enabled as a source.
Save the file.
This is exactly what solved the referenced case: removing a stale company NuGet source from NuGet.config allowed ABP Studio to install its CLI/tools again.
Volo.Abp.Studio.Cli can be installedOpen a terminal/command prompt and run:
dotnet tool install -g Volo.Abp.Studio.Cli
or, if it’s already installed:
dotnet tool update -g Volo.Abp.Studio.Cli
These commands must complete successfully and download from nuget.org.
If they still fail, the error message will usually point again to a NuGet feed issue; correct it as in step 1.
Checking pre-requirementsInstalling/using ABP Studio CLIJob 'DbMigrator'Opening ABP SuiteIf your NuGet configuration is healthy, DbMigrator should complete and ABP Suite should open in a tab without the “Couldn’t open ABP Suite” dialog.
If after fixing NuGet you still get failures:
Open the folder:
C:\Users\<YourUser>\.abp\studio\ui\logs/home/<user>/.abp/studio/ui/logs (or similar ~/.abp/studio/ui/logs)Open the latest log file and search for:
DbMigratorABP Suite.NET tool Volo.Abp.Studio.Cli installation failedIf you see errors referring to a specific NuGet source or network issue, you still have a NuGet configuration problem; correct the URLs or remove those sources and retry.
If the log shows a different error unrelated to NuGet (e.g., missing SDK, permission issue), address that message accordingly (install the referenced .NET SDK, fix permissions, etc.).
NuGet.config breaking ABP Studio CLI / DbMigrator):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.
To get that layout in ABP v10 you must implement your own custom layout based on LeptonX:
Copy an existing LeptonX layout
_Layout (side‑menu layout) and its related partials from the theme package into your app’s Pages (or Views) and register it as your layout.Move the toolbar rendering to the top header
LeptonXToolbars.Main into the top header area of your custom layout.LeptonXToolbars.Main wherever you want inside your custom layout.Use component / layout replacement
There is no simpler config switch; this requires a custom layout implementation.
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.
I’ll separate the answer into:
Goal:
With:
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.multitenancydemo.local");
});
ABP behaves as:
multitenancydemo.local → no tenant → Host context (CurrentTenant.IsAvailable == false).test.multitenancydemo.local → tenant test.This is correct and does not prevent host login. If:
then the usual root cause is OpenIddict client configuration / redirect URIs, not the user itself.
For host login to work:
Make sure the host domain is registered as a redirect URI & CORS origin for the Angular app client.
In OpenIddict (ABP 9+), this is done by the data seed (usually in YourProjectNameDomainDataSeedContributor, via OpenIddictApplicationManager).
You must ensure your Angular client (e.g. DentPal_App) has:
Redirect URIs:
- http://multitenancydemo.local:4200 (host)
- http://{0}.multitenancydemo.local:4200 (tenants via wildcard)
Post Logout Redirect URIs:
- same pattern as above
CORS origins:
- http://multitenancydemo.local:4200
- http://*.multitenancydemo.local:4200 (or each tenant explicitly)
In the blog/sample this is done by seeding with RootUrl = "http://{0}.multitenancydemo.local:4200".
You must add another entry (or additional redirect URI) for the host root URL:
await CreateApplicationAsync(
name: "DentPal_App",
type: OpenIddictConstants.ClientTypes.Public,
consentType: OpenIddictConstants.ConsentTypes.Explicit,
displayName: "DentPal Angular Application",
secret: null,
grantTypes: new[] { OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.RefreshToken },
scopes: new[] { "DentPal", OpenIddictConstants.Scopes.OfflineAccess, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile, OpenIddictConstants.Scopes.Roles },
redirectUri: "http://{0}.multitenancydemo.local:4200", // tenants
extraRedirectUris: new[]
{
"http://multitenancydemo.local:4200" // host
},
postLogoutRedirectUri: "http://{0}.multitenancydemo.local:4200",
extraPostLogoutRedirectUris: new[]
{
"http://multitenancydemo.local:4200"
},
clientUri: null,
permissions: ...);
(Exact method name/signature may differ, but the idea is: client has both wildcard and host URLs.)
Angular environment for the host domain must match that host URL.
For the host Angular app (where Super Admin logs in):
const hostBaseUrl = 'http://multitenancydemo.local:4200';
export const environment = {
production: false,
application: {
baseUrl: hostBaseUrl,
name: 'DentPal',
},
oAuthConfig: {
issuer: 'http://multitenancydemo.local:44397', // your HttpApi.Host/OpenIddict endpoint
redirectUri: hostBaseUrl,
clientId: 'DentPal_App',
responseType: 'code',
scope: 'offline_access DentPal',
requireHttps: false
},
apis: {
default: {
url: 'http://multitenancydemo.local:44397',
rootNamespace: 'DentPal',
},
},
};
If the host Angular also uses {0}.multitenancydemo.local as baseUrl / redirectUri while you are on multitenancydemo.local, the generated redirect_uri won’t match OpenIddict’s registered URIs and the login flow gets stuck/loops.
Confirm AppUrlOptions configuration doesn’t crash.
If you override AppUrlOptions (e.g. in HttpApiHostModule.ConfigureServices) like:
Configure<AppUrlOptions>(options =>
{
options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
options.Applications["Angular"].RootUrl = configuration["App:AngularUrl"];
});
make sure App:AngularUrl exists in appsettings.json and is not null (otherwise you can get hidden 500s that appear as “stuck login”).
Your screenshot shows:
error_description: The specified 'redirect_uri' is not valid for this client application.
The URL in the bar is something like:
.../connect/authorize?client_id=DentPal_App&redirect_uri=http%3A%2F%2Ftest.multitenancydemo.local%3A4200...
This means:
redirectUri = 'http://{0}.multitenancydemo.local:4200'.http://test.multitenancydemo.local:4200 correctly produces
redirect_uri=http://test.multitenancydemo.local:4200.DentPal_App does not have this redirect URI registered (only the non-tenant URL, or only another domain).To fix it for all tenants:
Enable wildcard domain support for OpenIddict (as in the article):
In YourProjectNameHttpApiHostModule.PreConfigureServices:
using Volo.Abp.OpenIddict.WildcardDomains;
public override void PreConfigureServices(ServiceConfigurationContext context)
{
#if DEBUG
PreConfigure<OpenIddictServerBuilder>(options =>
{
options.UseAspNetCore().DisableTransportSecurityRequirement();
});
#endif
PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
{
options.EnableWildcardDomainSupport = true;
options.WildcardDomainsFormat.Add("http://{0}.multitenancydemo.local:4200");
});
}
This lets OpenIddict treat http://{0}.multitenancydemo.local:4200 as a wildcard redirect URI pattern.
Seed the OpenIddict client with the wildcard RootUrl.
In your DbMigrator appsettings.json (or wherever you seed applications):
"OpenIddict": {
"Applications": {
"DentPal_App": {
"ClientId": "DentPal_App",
"RootUrl": "http://{0}.multitenancydemo.local:4200",
"Scopes": "DentPal offline_access",
"ClientType": "Public"
}
}
}
Or equivalent C# seeding where RootUrl = "http://{0}.multitenancydemo.local:4200".
Then run the DbMigrator again so the OpenIddictApps / OpenIddictRedirectUris tables are updated.
Angular tenant environment must use the same pattern.
For the tenant-specific Angular app (the one accessed via http://{tenant}.multitenancydemo.local:4200):
const tenantBaseUrl = 'http://{0}.multitenancydemo.local:4200';
export const environment = {
production: false,
application: {
baseUrl: tenantBaseUrl,
name: 'DentPal',
},
oAuthConfig: {
issuer: 'http://multitenancydemo.local:44397', // same auth server (host) or shared auth
redirectUri: tenantBaseUrl, // the {0} pattern
clientId: 'DentPal_App',
responseType: 'code',
scope: 'offline_access DentPal',
requireHttps: false
},
apis: {
default: {
url: 'http://multitenancydemo.local:44397', // API host; tenant is resolved via domain resolver on backend
rootNamespace: 'DentPal',
},
},
};
When the app runs at http://test.multitenancydemo.local:4200, the {0} placeholder resolves to test, and the generated redirect_uri will be:
http://test.multitenancydemo.local:4200
which matches the wildcard pattern allowed by AbpOpenIddictWildcardDomainOptions + RootUrl.
Tenant resolution configuration (already done, just confirm).
In HttpApiHostModule:
using Volo.Abp.MultiTenancy;
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.multitenancydemo.local");
});
}
No extra config is needed on Angular for tenant resolution; ABP Angular will automatically send the correct __tenant header based on the baseUrl/domain.
With ABP 9.0.4 + Angular + OpenIddict + domain-based resolver:
Redirect URIs must exactly match what OpenIddict knows.
AbpOpenIddictWildcardDomainOptions) rather than trying to store https://*.domain in DB (OpenIddict will reject *).You typically need both:
http://{0}.multitenancydemo.local:4200),http://multitenancydemo.local:4200) for Super Admin.If Angular is configured to auto-login in a component (like calling navigateToLogin() on component init), this can create loops if any mismatch exists. For debugging, keep auto-login disabled and trigger login only via a button until everything works.
Changing the host admin’s username to SuperAdmin is fine and not related, as long as:
Domain-based tenant resolver with Angular + OpenIddict (the blog you followed; includes wildcard configuration and sample repo):
ABP multi-tenancy docs (domain/subdomain resolver & {0} placeholder):
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.
The flow on your AuthServer is actually succeeding (authorization code + token). The failure happens after that, when the Blazor WebApp’s OpenIdConnectHandler tries to call /connect/userinfo:
The userinfo request was rejected because the mandatory 'access_token' parameter was missing.OpenIdConnectHandler.GetUserInformationAsync(...) -> HttpRequestException 401 UnauthorizedSo:
GetClaimsFromUserInfoEndpoint = true./connect/userinfo (most commonly because tokens are not saved).There are two practical ways to fix this for a Blazor WebApp with ABP/OpenIddict.
In ABP with OpenIddict, the standard user claims (subject, name, email, roles, etc.) are already available in the ID token, as long as you request the right scopes. In most cases you don’t need /connect/userinfo at all.
Configure your Blazor WebApp authentication like this (Program.cs / Startup):
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority = configuration["AuthServer:Authority"]; // e.g. https://auth.staging.myproject.io
options.RequireHttpsMetadata = true;
options.ClientId = "BlazorWebApp";
options.ClientSecret = configuration["AuthServer:ClientSecret"]; // your client secret
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
// Tokens can still be saved if you want to call APIs later
options.SaveTokens = true;
// IMPORTANT: disable UserInfo call to avoid your 401 error
options.GetClaimsFromUserInfoEndpoint = false;
// Request the scopes that ABP/OpenIddict uses to put claims into id_token
options.Scope.Clear();
options.Scope.Add("openid");
options.Scope.Add("profile");
options.Scope.Add("email");
options.Scope.Add("phone");
options.Scope.Add("roles");
options.Scope.Add("offline_access");
// Plus any API scopes you need, e.g.:
options.Scope.Add("AuthServer");
options.Scope.Add("IdentityService");
options.Scope.Add("AdministrationService");
options.Scope.Add("SaasService");
options.Scope.Add("AuditLoggingService");
options.Scope.Add("GdprService");
options.Scope.Add("ChatService");
});
Also ensure that in your AuthServer/OpenIddict client definition for "BlazorWebApp" the same scopes are allowed.
With GetClaimsFromUserInfoEndpoint = false, the handler will not call /connect/userinfo, so the missing_token / 401 error disappears, and the Blazor WebApp uses the claims in id_token.
If you explicitly want to use /connect/userinfo:
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
"BlazorWebApp" client in AuthServer:
But since ABP/OpenIddict already includes the usual claims in id_token, Option A is usually simpler and more robust, especially when migrating from Blazor WASM to Blazor WebApp.
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.
You’re running into two different, but related, problems:
Below is what is confirmed to work with ABP + OpenIddict in multi-tenant setups and should address your two concrete follow‑up issues.
invalid_grant when not setting ctx.Options.ClientIdThis OpenIddict error:
{ "error": "invalid_grant",
"error_description": "The specified authorization code cannot be used by this client application.",
"error_uri": "https://documentation.openiddict.com/errors/ID2069"
}
happens when the client ID used at the token endpoint doesn’t match the one that got the authorization code.
In the auth code flow with OpenIdConnect handler:
/connect/authorize) uses a client_id that comes from:
OpenIdConnectOptions.ClientId by default, unless overridden by ProtocolMessage.ClientId on redirect./connect/token) always uses OpenIdConnectOptions.ClientId (the options’ value), not the ProtocolMessage you set during redirect.That means:
ClientId only on ProtocolMessage (e.g., ctx.ProtocolMessage.ClientId) but keep options.ClientId unchanged, authorize and token endpoints will use different client IDs:
invalid_grant / ID2069.For multi-tenant with per-tenant ClientId, you have 2 realistic options:
With one OpenIdConnect scheme, you can only have one actual ClientId that participates correctly in both authorize and token stages (because options.ClientId is global to the scheme). You may alter authority/issuer/redirect dynamically, but not the client id per tenant without hacks.
So if you need per-tenant domain but same client id (e.g., a single OpenIddict application with wildcard redirect URIs), you can:
options.ClientId fixed.AbpOpenIddictWildcardDomainOptions) in AuthServer to accept multiple domains.ctx.ProtocolMessage.IssuerAddress based on tenant; don’t touch ClientId.This avoids invalid_grant.
If each tenant truly must have its own ClientId, the clean way is:
ClientId and Authority.Pattern:
// Configure all known (static) tenants
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "oidc-default"; // or dynamically choose in code
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect("oidc-tenantA", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://auth-for-tenantA.example.com";
options.ClientId = "tenantA_client";
options.ResponseType = "code";
// ...
})
.AddOpenIdConnect("oidc-tenantB", options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = "https://auth-for-tenantB.example.com";
options.ClientId = "tenantB_client";
options.ResponseType = "code";
// ...
});
Then:
return Challenge(
new AuthenticationProperties { RedirectUri = "/" },
$"oidc-{currentTenantName}");
This way, each scheme’s options.ClientId is consistent across authorize and token requests. You can still compute Authority from your per‑tenant host mapping.
Given your constraints (no DB on Blazor server, fully different domains), this is usually the most robust approach.
Invalid_issuer only for the custom controller + YARPFacts from your description:
Invalid_issuer.Owl.TokenWildcardIssuerValidator) and it is called for app services but not for this custom controller.There are two sides to this:
Base class doesn’t matter. A controller that derives from a 3rd‑party base class works fine with [Authorize], as long as:
IssuerValidator) applies.You need to verify:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = configuration["AuthServer:Authority"]; // maybe wildcard format
options.RequireHttpsMetadata = true;
options.TokenValidationParameters.ValidateIssuer = true;
options.TokenValidationParameters.IssuerValidator =
TokenWildcardIssuerValidator.IssuerValidator; // or your own
});
[Authorize] // DO NOT override AuthenticationSchemes here unless necessary
[Route("api/third-party")]
public class MyThirdPartyController : Required3rdPartyBaseController
{
// ...
}
If you write:
[Authorize(AuthenticationSchemes = "Bearer")]
but your AddJwtBearer is registered only as the default scheme (or under a different name), your custom controller might end up using a different bearer scheme without the IssuerValidator. Result: different behavior versus ABP controllers.
To prove this, log the user in that controller:
[HttpGet("debug-auth")]
public IActionResult DebugAuth()
{
var user = HttpContext.User;
return Ok(new {
authenticated = user.Identity?.IsAuthenticated,
name = user.Identity?.Name,
authType = user.Identity?.AuthenticationType
});
}
If authenticated is false, or authType doesn’t match your expected scheme, that’s where the mismatch is.
In ABP, all controllers (ABP or your own) share the same authentication pipeline as long as you don’t override schemes per-controller or per-policy.
The Invalid_issuer you mentioned is almost certainly thrown by the downstream service, not your ABP host, because:
That means:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = true;
options.TokenValidationParameters.ValidateIssuer = true;
// Either a list of issuers:
options.TokenValidationParameters.ValidIssuers = new[]
{
"https://tenantA-auth.example.com/",
"https://tenantB-auth.example.com/"
};
// Or the same custom validator you registered in the ABP host:
options.TokenValidationParameters.IssuerValidator =
TokenWildcardIssuerValidator.IssuerValidator; // or your custom implementation
});
If you don’t control the downstream service (3rd‑party), that service must be configured to trust your OpenIddict issuer(s); otherwise, no amount of work in your ABP host will fix its Invalid_issuer.
With YARP:
Authorization header is forwarded.ReverseProxy config.Example route:
"Routes": {
"TenantService": {
"ClusterId": "TenantCluster",
"Match": {
"Path": "/api/thirdparty/{**catch-all}"
}
// No header transforms that strip Authorization
}
}
Make sure there’s no transform like:
"Transforms": [
{ "RequestHeadersCopy": "false" }
]
without then explicitly re‑adding Authorization.
For per-tenant ClientId:
AbpOpenIddictWildcardDomainOptions) and domain‑based tenant resolution only. Then you don’t change ClientId at all and avoid invalid_grant.ClientId/Authority.options.ClientId at runtime for a single scheme; it will always mismatch on the token endpoint at some point.For the custom controller + YARP:
AuthenticationSchemes unless you duplicate the full JwtBearer config).DefaultAuthenticateScheme and DefaultChallengeScheme).Owl.TokenWildcardIssuerValidator).Multi-tenancy and tenant resolution:
https://abp.io/docs/latest/framework/architecture/multi-tenancy
OpenIddict integration in ABP (incl. wildcard domain support):
https://abp.io/docs/latest/modules/openiddict
Wildcard issuer validator library (often used with multi-tenant issuers):
https://github.com/maliming/Owl.TokenWildcardIssuerValidator
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.