Activities of "AI-Bot"

1. Solution

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:

  • AuthServer log:
    The userinfo request was rejected because the mandatory 'access_token' parameter was missing.
  • Blazor WebApp log:
    OpenIdConnectHandler.GetUserInformationAsync(...) -> HttpRequestException 401 Unauthorized

So:

  • The Blazor WebApp is configured with GetClaimsFromUserInfoEndpoint = true.
  • But the handler is not sending an access token to /connect/userinfo (most commonly because tokens are not saved).

There are two practical ways to fix this for a Blazor WebApp with ABP/OpenIddict.


Option A (recommended): Stop calling the UserInfo endpoint

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.


Option B: Keep UserInfo and send a valid access token

If you explicitly want to use /connect/userinfo:

  1. Make sure the WebApp saves tokens:
options.SaveTokens = true;
   options.GetClaimsFromUserInfoEndpoint = true;
  1. Keep the same scopes as in option A (so an access token is actually issued).
  2. Ensure your "BlazorWebApp" client in AuthServer:
    • Has those scopes assigned.
    • Issues an access token with an audience accepted by AuthServer’s OpenIddict validation (in your current log the first issue is simply missing token; if you later see “token had no valid audience”, that’s an audience/scope configuration problem).

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.


2. Sources

  • ABP OpenIddict deployment & validation/audience notes (relevant when using access tokens and UserInfo):
    • https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/openiddict-deployment

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.

  1. Solution

You’re running into two different, but related, problems:

  • Per-tenant OIDC client/authority for Blazor Web App (authorization code + PKCE).
  • Token validation differences between your ABP stack and a custom/YARP-forwarded endpoint.

Below is what is confirmed to work with ABP + OpenIddict in multi-tenant setups and should address your two concrete follow‑up issues.


1) invalid_grant when not setting ctx.Options.ClientId

This 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:

  • The authorization request (redirect to /connect/authorize) uses a client_id that comes from:
    • OpenIdConnectOptions.ClientId by default, unless overridden by ProtocolMessage.ClientId on redirect.
  • The token request (POST /connect/token) always uses OpenIdConnectOptions.ClientId (the options’ value), not the ProtocolMessage you set during redirect.

That means:

  • If you set a per-tenant ClientId only on ProtocolMessage (e.g., ctx.ProtocolMessage.ClientId) but keep options.ClientId unchanged, authorize and token endpoints will use different client IDs:
    • authorize: per-tenant client id
    • token: default client id
  • Result: invalid_grant / ID2069.

For multi-tenant with per-tenant ClientId, you have 2 realistic options:

Option A – One OIDC scheme (global) + per-tenant ClientId not supported

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:

  • Keep options.ClientId fixed.
  • Use wildcard redirect URIs (AbpOpenIddictWildcardDomainOptions) in AuthServer to accept multiple domains.
  • Set only ctx.ProtocolMessage.IssuerAddress based on tenant; don’t touch ClientId.

This avoids invalid_grant.

Option B – Per-tenant OIDC schemes (recommended when ClientId must differ)

If each tenant truly must have its own ClientId, the clean way is:

  • Define one OpenIdConnect scheme per tenant, each with its own ClientId and Authority.
  • Pick the scheme at sign‑in time based on the current tenant.

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:

  • In your login UI, challenge the correct scheme:
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.


2) 401/302 and Invalid_issuer only for the custom controller + YARP

Facts from your description:

  • Tokens issued by your embedded OpenIddict server work for “normal” ABP application services and generated proxies.
  • When the same token is forwarded via YARP from a custom controller to a tenant‑specific external URL, you get 401/302 and sometimes Invalid_issuer.
  • You already plugged a custom issuer validator (similar to Owl.TokenWildcardIssuerValidator) and it is called for app services but not for this custom controller.

There are two sides to this:

2.1. Authentication in your own host (custom controller)

Base class doesn’t matter. A controller that derives from a 3rd‑party base class works fine with [Authorize], as long as:

  • The same authentication scheme is used.
  • The same JWT bearer / OpenIddict validation configuration (including IssuerValidator) applies.

You need to verify:

  1. Global auth configuration (in your HttpApi.Host / same host as the controller):
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
    });
  1. Controller attributes:
[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.

2.2. Downstream tenant-specific service (where YARP forwards the request)

The Invalid_issuer you mentioned is almost certainly thrown by the downstream service, not your ABP host, because:

  • Token is accepted by your ABP application services.
  • Same token, when forwarded, gets rejected there.

That means:

  • On the downstream service (tenant‑specific API), you must configure JWT/OpenIddict validation in the same way as in your main ABP host:
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.

2.3. YARP specifics

With YARP:

  • By default, the Authorization header is forwarded.
  • Ensure you have no transforms that drop or change it in your 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.


What to change concretely based on your two “followup errors”

  1. For per-tenant ClientId:

    • If you can, prefer one ClientId across tenants and use wildcard redirect URIs (AbpOpenIddictWildcardDomainOptions) and domain‑based tenant resolution only. Then you don’t change ClientId at all and avoid invalid_grant.
    • If you must have different ClientIds:
      • Configure multiple OpenIdConnect schemes, one per tenant, each with its own ClientId/Authority.
      • Challenge the correct scheme based on the resolved tenant (from domain/ICurrentTenant).
      • Do not try to change options.ClientId at runtime for a single scheme; it will always mismatch on the token endpoint at some point.
  2. For the custom controller + YARP:

    • Ensure the controller uses the same authentication scheme as your normal APIs (don’t override AuthenticationSchemes unless you duplicate the full JwtBearer config).
    • Confirm your custom IssuerValidator is attached to the exact JwtBearer scheme that’s being used (DefaultAuthenticateScheme and DefaultChallengeScheme).
    • On the downstream service, configure JWT validation (or OpenIddict validation) to accept the same issuer(s), or use the same wildcard issuer validator (for example Owl.TokenWildcardIssuerValidator).

  1. Sources
  • 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.

  1. Per-tenant ClientId / Authority in Blazor Web App

The behavior you’re seeing is expected:

  • OpenIdConnectOptions is registered as a singleton per scheme.
  • When you set ctx.Options.ClientId / ctx.Options.Authority inside an event (e.g. OnRedirectToIdentityProvider), you are mutating that singleton.
  • That change then applies to all subsequent requests, so the last-used tenant “wins”.

To make ClientId and Authority per-request and per-tenant:

  • Do not change ctx.Options.* in the event.
  • Instead, set values on ctx.ProtocolMessage, which is specific to the current redirect.

Example:

using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options;
using Volo.Abp.MultiTenancy;

public class TenantOpenIdConnectEvents : IConfigureNamedOptions<OpenIdConnectOptions>
{
    private readonly IConfiguration _configuration;
    private readonly ICurrentTenant _currentTenant;

    public TenantOpenIdConnectEvents(
        IConfiguration configuration,
        ICurrentTenant currentTenant)
    {
        _configuration = configuration;
        _currentTenant = currentTenant;
    }

    public void Configure(string name, OpenIdConnectOptions options)
    {
        if (name != "oidc") return; // your scheme name

        options.Events ??= new OpenIdConnectEvents();

        var existingRedirect = options.Events.OnRedirectToIdentityProvider;
        options.Events.OnRedirectToIdentityProvider = async context =>
        {
            // Determine tenant (you said URL-based tenant resolution already works)
            var tenantName = _currentTenant.Name; // or from Host header if needed

            // Get per-tenant settings from configuration (or any store) –
            // no DB needed on the Blazor server, just a mapping.
            // appsettings.json:
            // "TenantAuth": {
            //   "tenantA": { "Authority": "https://auth-a.example.com", "ClientId": "ClientA" },
            //   "tenantB": { "Authority": "https://auth-b.example.com", "ClientId": "ClientB" }
            // }
            var section = _configuration.GetSection("TenantAuth");
            if (!tenantName.IsNullOrWhiteSpace())
            {
                var tenantSection = section.GetSection(tenantName!);
                var authority = tenantSection["Authority"];
                var clientId = tenantSection["ClientId"];

                if (!string.IsNullOrEmpty(authority))
                {
                    // Set per-request issuer address for this redirect
                    context.ProtocolMessage.IssuerAddress =
                        authority.TrimEnd('/') + "/connect/authorize";
                }

                if (!string.IsNullOrEmpty(clientId))
                {
                    // Set per-request client id – DO NOT touch options.ClientId here
                    context.ProtocolMessage.ClientId = clientId;
                }
            }

            if (existingRedirect != null)
            {
                await existingRedirect(context);
            }
        };
    }

    public void Configure(OpenIdConnectOptions options)
        => Configure(Options.DefaultName, options);
}

Register:

services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, TenantOpenIdConnectEvents>();

Key points:

  • options.ClientId / options.Authority stay as a default (e.g. host or fallback).
  • For each redirect, you compute the current tenant, then override:
    • context.ProtocolMessage.ClientId
    • context.ProtocolMessage.IssuerAddress
  • No “stickiness” across tenants, because you didn’t mutate the singleton options.

This pattern works fine even when the Blazor server has no DB access; just keep a configuration-based map from tenant (or domain) → Authority / ClientId.

Relevant docs for how ABP wires OIDC (for reference of where these events fit):

  • https://abp.io/docs/latest/framework/architecture/multi-tenancy#tenant-resolution
  • https://abp.io/docs/latest/modules/openiddict
  1. Custom controller + YARP + Invalid_issuer / 401

A few important clarifications:

  • Authorization does not depend on AbpController / ApplicationService base classes.
    [Authorize] works on any controller; only the authentication configuration and attributes matter.
  • If your custom controller is in the same host as your app services:
    • Incoming token validation is done by the same authentication schemes you configured in AddAuthentication / AddJwtBearer or OpenIddict validation.
    • If your custom IssuerValidator works for app services but never fires for this controller, then that controller is not using the same authentication scheme.

2.1. Ensure the same authentication scheme is used

Check:

  • Your global authentication configuration:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // your issuer / audience settings
        options.RequireHttpsMetadata = true;

        // Custom issuer validator – MUST be here
        options.TokenValidationParameters.ValidAudience = "your-api";
        options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) =>
        {
            // your custom logic, e.g. per-tenant issuers
            // return a valid issuer string or throw
            return issuer;
        };
    });
  • Your custom controller:
[Authorize] // IMPORTANT: do NOT override the scheme unless you really need to
public class ThirdPartyController : ThirdPartyBaseController
{
    // ...
}

Common pitfalls:

  • Using [Authorize(AuthenticationSchemes = "Bearer")] while you configured the custom IssuerValidator only on the default scheme (JwtBearerDefaults.AuthenticationScheme), or vice versa.
  • Using a different policy that binds to a different scheme.
  • Having multiple AddJwtBearer(...) calls with different scheme names and only configuring the IssuerValidator on one.

Make sure:

  • The scheme used by the [Authorize] on your custom controller is the one where you attached the custom IssuerValidator.

2.2. YARP forwarding and Invalid_issuer from the downstream service

From your description:

  • The custom controller doesn’t descend from ABP base classes.
  • It uses YARP to forward the incoming access token to a tenant-specific external URL.
  • That external service responds with 401, 302, sometimes Invalid_issuer.

This means:

  • The token you forward is valid (works with ABP-generated proxies in other parts).
  • The downstream (tenant-specific) service rejects it, and its JWT validation configuration is where Invalid_issuer occurs.

Checklist:

  1. Ensure YARP is preserving the Authorization header as-is:
services.AddReverseProxy()
       .LoadFromConfig(Configuration.GetSection("ReverseProxy"));
   // default YARP preserves Authorization; only custom transforms may strip it
  1. On the downstream service (tenant-specific API), configure the same custom IssuerValidator and issuer list as you did for your main services:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
       .AddJwtBearer(options =>
       {
           options.RequireHttpsMetadata = true;
           options.TokenValidationParameters.ValidateIssuer = true;

           // Either allow all tenant issuers:
           options.TokenValidationParameters.ValidIssuers = new[]
           {
               "https://auth-tenantA.example.com/",
               "https://auth-tenantB.example.com/",
               // ...
           };

           // Or use the same custom validator you use in your ABP host:
           options.TokenValidationParameters.IssuerValidator = (issuer, token, parameters) =>
           {
               // same logic as ABP host, e.g. look up tenant by issuer
               return issuer;
           };
       });

If you’re using OpenIddict validation instead of pure JwtBearer, configure it similarly on that service.

  1. Confirm that the downstream service is not using a different scheme (e.g. AddJwtBearer("SomeScheme", ...)) while callers send a token to the default Bearer scheme without the same IssuerValidator.

2.3. Why the custom validator is “never called” for the custom controller

There are only a few reasons this can happen inside the same host:

  • The request is not authenticated using the scheme where the validator is defined (different scheme name in [Authorize] / default scheme).
  • Authentication is short-circuited before JWT validation (e.g. another scheme challenged first).
  • The custom controller endpoint has its own [AllowAnonymous] (not your case) or uses a policy mapped to a different scheme.

Action steps:

  1. In your startup, log which scheme is the default:
services.AddAuthentication(options =>
   {
       options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
       options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
   })
   .AddJwtBearer(...);
  1. On the custom controller, temporarily add:
[Authorize]
   [ApiController]
   [Route("api/thirdparty")]
   public class ThirdPartyController : ThirdPartyBaseController
   {
       private readonly IHttpContextAccessor _httpContextAccessor;

       public ThirdPartyController(IHttpContextAccessor httpContextAccessor)
       {
           _httpContextAccessor = httpContextAccessor;
       }

       [HttpGet("debug-auth")]
       public IActionResult DebugAuth()
       {
           var user = _httpContextAccessor.HttpContext!.User;
           // Inspect claims/principal to confirm which scheme authenticated it
           return Ok(new { authenticated = user.Identity?.IsAuthenticated, name = user.Identity?.Name });
       }
   }

If IsAuthenticated is false here while it’s true on app services for the same token, that confirms the scheme mismatch problem.


Summary

  1. Per-tenant OIDC config in Blazor Web App:

    • Don’t modify ctx.Options.ClientId or .Authority inside events; that changes them globally.
    • Instead, compute per-tenant values and set:
      • ctx.ProtocolMessage.ClientId
      • ctx.ProtocolMessage.IssuerAddress
    • Source tenant name from ICurrentTenant or domain, and keep a config mapping for each tenant’s Authority / ClientId.
  2. Custom controller + YARP + Invalid_issuer:

    • Base class (AbpController vs 3rd-party base) is irrelevant to authentication.
    • Ensure the controller uses the same authentication scheme where your custom IssuerValidator is configured.
    • The Invalid_issuer you see is thrown by the downstream tenant-specific service; configure the same issuer validation (ValidIssuers or IssuerValidator) there.
    • Verify YARP preserves the Authorization header and that the downstream API’s auth configuration matches the expectations of the token’s issuer.

Sources (for patterns used above):

  • https://abp.io/docs/latest/framework/architecture/multi-tenancy#tenant-resolution
  • https://abp.io/docs/latest/framework/architecture/multi-tenancy
  • https://abp.io/docs/latest/modules/openiddict

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.

  1. What is happening
  • MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser) builds a cache key per user (tenant + user id + culture).
  • MvcCurrentApplicationConfigurationCacheResetEventHandler.HandleEventAsync does:
public virtual async Task HandleEventAsync(CurrentApplicationConfigurationCacheResetEventData eventData)
  {
      await Cache.RemoveAsync(CreateCacheKey());
  }

  protected virtual string CreateCacheKey()
  {
      return MvcCachedApplicationConfigurationClientHelper.CreateCacheKey(CurrentUser);
  }
  • CurrentUser here is the user who is executing the current request (the one who publishes CurrentApplicationConfigurationCacheResetEventData).

So when your admin user changes settings/permissions and triggers the reset event, the handler removes only the admin’s cache entry, because CurrentUser is admin.
Other users’ cache keys remain in Redis and expire only when the cache timeout (5 minutes) is reached. That matches exactly what you’re seeing: “deleted is always admin, other users can’t be deleted, only wait 5 minutes timeout”.

This is by design: CurrentApplicationConfigurationCacheResetEventData is meant to reset the current user’s application configuration, not all users.


  1. Why you cannot invalidate other users’ caches from admin
  • The application‑configuration cache is per client / per user.
  • The server does not know which user is currently using which cache key on which browser/instance; the only safe thing it can do synchronously is “invalidate current user”.
  • Therefore, when admin changes other users’ permissions, ABP does not automatically clear those users’ configuration caches. They get fresh config only when:
    • Their own client calls /api/abp/application-configuration again and refreshes its cache, or
    • The existing cache entry expires.

  1. Options / Workarounds

Choose depending on your requirement:

A. Accept per-user behavior (recommended, simplest)

If it is acceptable that other users see updated permissions after at most 5 minutes (or after they refresh / re-login), then you don’t need any change. This is ABP’s default behavior.

You can lower the cache duration if 5 minutes is too long by customizing distributed cache options for ApplicationConfigurationDto, but that increases load:

Configure<AbpDistributedCacheOptions>(options =>
{
    options.CacheConfigurators.Add(cacheName =>
    {
        if (cacheName == typeof(ApplicationConfigurationDto).FullName)
        {
            return entryOpts =>
            {
                // e.g. 1 minute
                entryOpts.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
            };
        }

        return null;
    });
});

B. Disable application-configuration caching completely

If you prefer always fresh config and can accept more backend traffic:

Configure<AbpAspNetCoreMvcClientOptions>(options =>
{
    options.CacheApplicationConfiguration = false;
});

Then MvcCurrentApplicationConfigurationCacheResetEventHandler effectively becomes unnecessary because nothing is cached.


C. Implement your own global cache invalidation (advanced, not built-in)

If you really need “when admin changes something, all users must see it immediately”:

  1. Keep using distributed cache, but:

    • Do not use a per-user cache key.
    • Or maintain an additional “version” value in cache and include it in the key to force global refresh.
  2. That would require:

    • Replacing the default MvcCachedApplicationConfigurationClient (or helper) to use your own cache key strategy.
    • Possibly writing a custom event handler that either:
      • Clears a global key, or
      • Increments a “configuration version” value that becomes part of per-user keys.

This is not provided out-of-the-box; you must implement and test it yourself.


  1. Answer to your direct question
  • MvcCurrentApplicationConfigurationCacheResetEventHandler.HandleEventAsync always uses the CurrentUser of the request that fired the event.
  • In your scenario, that is always the admin user operating in the back office, therefore only admin’s cache entries are removed.
  • Other users’ application configuration cache entries are not removed by this event and will only refresh when:
    • Their own client re-requests configuration, or
    • The cache expiration time is reached.

If you need immediate reflection for other users, use option B (disable cache) or implement a custom global invalidation strategy as in option C.


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.

  1. Yes, you can do this
    You can absolutely have:
  • One Auth Server (MainProject)
  • Multiple API Hosts (MainProject API Host + ChildProject API Host)
  • Use [Authorize(ChildProjectPermissions.Something)] on ChildProject endpoints

…as long as both API hosts validate the same access token correctly and the permissions are defined/seeded consistently.

Your 401 means the ChildProject is not accepting the token (authentication fails) before it even reaches the ABP permission system.


  1. Configure ChildProject to trust MainProject Auth Server

In ChildProject.HttpApi.Host:

appsettings.json

"AuthServer": {
  "Authority": "https://MAIN-PROJECT-AUTH-URL",   // MainProject Auth server URL
  "RequireHttpsMetadata": "false",                // true in production with HTTPS
  "SwaggerClientId": "ChildProject_Swagger"       // optional, for swagger auth
},
"Jwt": {
  "Authority": "https://MAIN-PROJECT-AUTH-URL",
  "Audience": "ChildProject"                      // see step 3
}

(or follow the same AuthServer section structure you already use in MainProject HttpApi.Host, just changing the Audience if needed.)

HttpApiHostModule – ConfigureServices

public override void ConfigureServices(ServiceConfigurationContext context)
{
    var configuration = context.Services.GetRequiredService<IConfiguration>();

    ConfigureAuthentication(context, configuration);
}

private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.AddAuthentication("Bearer")
        .AddJwtBearer("Bearer", options =>
        {
            options.Authority = configuration["AuthServer:Authority"];
            options.RequireHttpsMetadata = 
                Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);

            options.Audience = "ChildProject"; // must match the audience of the token
            // (temporarily you can disable audience validation to test)
            // options.TokenValidationParameters.ValidateAudience = false;
        });
}

Startup pipeline (OnApplicationInitialization)

Make sure the middleware order includes both authentication and authorization:

public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
    var app = context.GetApplicationBuilder();

    app.UseCorrelationId();
    app.UseRouting();
    app.UseCors();

    app.UseAuthentication();     // BEFORE multi-tenancy & authorization
    app.UseAbpRequestLocalization();
    app.UseMultiTenancy();
    app.UseAuthorization();

    app.UseSwagger();
    app.UseAbpSwaggerUI(options => { ... });

    app.UseAuditing();
    app.UseConfiguredEndpoints();
}

If UseAuthentication() is missing or in wrong order, you will also constantly get 401.


  1. Make sure the token is valid for ChildProject

The token you get from MainProject must be accepted by ChildProject:

  • In MainProject Auth Server (OpenIddict data seed / identity seed contributor), you must define an API scope / resource that represents ChildProject, for example "ChildProject".

  • Add that scope to the client that obtains the token (Angular, Swagger, Postman client, etc.).

  • When you request a token, request that scope:

    • For Swagger/OpenID: include ChildProject in the scopes.
    • For Postman: scope=... ChildProject.
  • The resulting token must either:

    • Contain aud = "ChildProject", and you set options.Audience = "ChildProject", or
    • You set ValidateAudience = false (for testing only) so that any token from the same authority is accepted.

If the aud in the token doesn’t match what ChildProject expects, you will see 401 from the JWT middleware, even though the same token works on the MainProject API Host.


  1. Share permission definitions between Main and Child projects

To be able to use:

[Authorize(xxxWebAPIPermissions.WCFService.Default)]
public class MyChildController : AbpController
{
    ...
}

do the following:

  1. Permission definition provider
    Put your xxxWebAPIPermissions and the corresponding PermissionDefinitionProvider in a shared application contracts project that is referenced by:

    • MainProject Auth Server (so permissions can be assigned to roles/users)
    • MainProject API Host
    • ChildProject API Host
  2. Module dependency

    In ChildProjectApplicationContractsModule (or HttpApi module), depend on the module that contains the permission provider:

[DependsOn(
       typeof(AbpAuthorizationAbstractionsModule),
       typeof(MySharedPermissionsModule) // where your PermissionDefinitionProvider lives
   )]
   public class ChildProjectApplicationContractsModule : AbpModule
   {
   }
  1. Seed permissions in MainProject (as you already do).
    As long as the user has that permission (directly or via role), ABP’s permission checker in ChildProject will read it (claims + database) and authorize the policy.

If authentication is correct and the user really has the permission, then failing [Authorize(…)] would give 403 Forbidden, not 401.
Since you get 401, it’s almost certainly token validation (authentication) in ChildProject, not the permission policy itself.


  1. Quick checklist

On ChildProject HttpApi.Host:

  • [ ] UseAuthentication() is present and before UseAuthorization().
  • [ ] AddAuthentication().AddJwtBearer() is configured to use MainProject AuthServer Authority.
  • [ ] Audience and token’s aud (or ValidateAudience = false) are aligned.
  • [ ] Tokens requested include the ChildProject scope/audience.
  • [ ] Permission definitions project is shared and referenced.
  • [ ] Same database (you already have) so user, role, permission data are visible.

Once these are done, you can:

  1. Get token from MainProject Auth Server.
  2. Call ChildProject endpoints with Authorization: Bearer <token>.
  3. [Authorize(xxxWebAPIPermissions.WCFService.Default)] will work as expected.

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.

  1. Solution You’re right: on a unified Blazor Server host, depending on ChatBlazorServerModule should wire up the UI, API, and hub endpoints. The antiforgery error you’re seeing typically happens when the negotiate POST is treated as a normal MVC POST without the RequestVerificationToken header. In Blazor Server with cookies, ABP’s antiforgery normally works out-of-the-box. The usual causes and concrete fixes are:
  • Verify you run a single-origin Blazor Server host

    • The Blazor Server app that renders the UI must also host the chat hub at the same origin (scheme+host+port). If you’re proxying through a different host/port or a reverse proxy, the XSRF-TOKEN cookie may not be sent or recognized as same-site, causing 400.
    • Test by running your Blazor Server project directly (no gateway), then open browser devtools:
      • Confirm POST /signalr-hubs/chat/negotiate returns 200.
      • Confirm request carries cookies and response sets/has XSRF-TOKEN.
  • Don’t add the “access_token to Authorization” middleware

    • That snippet is only for SPA (Angular/Blazor WASM) using bearer tokens. You’re using cookie auth; remove that snippet if present anywhere.
  • Ensure ABP antiforgery defaults aren’t overridden

    • Remove custom/global ValidateAntiforgeryToken filters you may have added beyond ABP defaults.
    • Do not disable ABP’s application-configuration endpoint or change AntiforgeryOptions.HeaderName.
    • Make sure Configure<AbpAntiForgeryOptions> is not altered to enforce validation on non-browser clients or unexpected endpoints.
  • Confirm ChatSignalRModule is active in the same host

    • With ChatBlazorServerModule, it should already depend on SignalR and map /signalr-hubs/chat. Still, check your Blazor Server module class includes ChatBlazorServerModule:
using Volo.Abp.Modularity;
    using Volo.Chat.Blazor.Server;

    [DependsOn(typeof(ChatBlazorServerModule))]
    public class YourAppBlazorModule : AbpModule
    {
    }
  • Check app startup logs to see endpoint mapping for /signalr-hubs/chat.

  • Check SameSite and HTTPS

    • If you’re on HTTPS and any proxying is involved, ensure cookies are not dropped due to SameSite. For cross-site scenarios you’d need SameSite=None; Secure, but the recommended approach for Blazor Server is to keep everything same-site.
    • Clear browser cookies and re-login after enabling the feature/permissions.
  • Known ASP.NET Core SignalR option on Blazor Server

    • If you have hubs with implicit FromServices parameters, set:
Configure<Microsoft.AspNetCore.SignalR.HubOptions>(options =>
    {
        options.DisableImplicitFromServicesParameters = true;
    });
This is a general ASP.NET Core quirk; not normally required for the Chat module, but harmless if added.

If you align to a single Blazor Server host (no gateway), leave cookie auth only, and keep ABP antiforgery defaults, the negotiate POST will succeed and the chat toolbar component will connect without the antiforgery 400.

  1. Sources
  • https://abp.io/docs/latest/modules/chat
  • https://abp.io/docs/latest/framework/real-time/signalr
  • https://abp.io/docs/latest/framework/infrastructure/csrf-anti-forgery

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.

  1. Solution You’re on Blazor Server with cookie authentication, so you should NOT add the “Bearer token from access_token” middleware. That snippet is only required for SPA clients (Angular/Blazor WASM) because browsers can’t set Authorization headers for WebSockets/SSE and ABP uses access_token query for SignalR in those UIs.

Your error indicates the antiforgery protection blocked the SignalR negotiate request. In ABP, antiforgery is automatically enforced for browser-originated POSTs, and the client sends the RequestVerificationToken header automatically when using ABP’s HTTP pipeline. However, SignalR’s negotiate POST (from the chat client component) must also include the antiforgery token. In Blazor Server, this works when:

  • The Chat Blazor UI package is installed and used in the same host as the SignalR hub (cookie auth).
  • Antiforgery middleware is the standard ABP configuration (no custom global filters interfering).
  • The negotiate POST is same-site (no gateway/proxy domain causing cross-site conditions).

Do the following checks/fixes:

  • Ensure correct module dependencies in your Blazor Server host (single host)

    • Project YourApp.Blazor (the app you run):
      • Packages: Volo.Chat.Blazor, Volo.Chat.SignalR
      • Module class: [DependsOn(typeof(ChatBlazorModule), typeof(ChatSignalRModule))] public class YourAppBlazorModule : AbpModule { }
    • You do not need a separate HttpApi.Host when you’re running Blazor Server unified host.
  • Keep cookie auth only (no bearer conversion)

    • Remove any middleware that tries to map access_token to Authorization for /signalr-hubs/chat.
    • Cookie-based auth is enough for Blazor Server; SignalR hub will use the authenticated cookie.
  • Do not disable ABP antiforgery

    • ABP already sets XSRF-TOKEN cookie and expects RequestVerificationToken header; the built-in Blazor integration takes care of it for ABP components.
    • If you added custom [Auto/Validate]Antiforgery filters globally or changed ABP’s antiforgery options, revert to defaults.
  • Same-site and domain consistency

    • Run the Blazor Server site and the SignalR hub on the same origin (scheme+host+port). If you introduced a reverse proxy/gateway or different port/host, the negotiate POST may be treated as cross-site and blocked if the RequestVerificationToken header isn’t present or cookie isn’t sent due to SameSite rules.
    • For dev: run just the Blazor Server host; don’t front it with a gateway until it works.
    • If you must proxy, ensure cookie forwarding and SameSite=None;Secure for cookies over HTTPS.
  • Verify hub mapping and startup

    • Depending on ChatSignalRModule automatically maps the hub to /signalr-hubs/chat.
    • Check logs at app start for endpoint registration; ensure no errors.
  • Permissions and Feature are already set (you did this), but confirm the logged-in user stays authenticated during the negotiate call (no redirect/401). If your layout triggers a redirect for unauthenticated negotiate, the antiforgery error can follow.

After aligning the above, the negotiate POST should include antiforgery automatically in Blazor Server and succeed with cookie auth, and the chat toolbar component will connect without the 400.

  1. Sources
  • https://abp.io/docs/latest/modules/chat
  • https://abp.io/docs/latest/framework/real-time/signalr
  • https://abp.io/docs/latest/framework/infrastructure/csrf-anti-forgery

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.

  1. Solution To make the Chat module UI (toolbar button and menu) appear in a Blazor Server app (v9.1.3), verify these steps end-to-end:
  • Install the correct packages into the Blazor Server host

    • In your Blazor Server host project (YourApp.Blazor), ensure the UI and SignalR packages are installed:
      • Volo.Chat.Blazor
      • Volo.Chat.SignalR
    • In your HttpApi.Host (or unified host if not tiered), ensure:
      • Volo.Chat.HttpApi
      • Volo.Chat.Application
      • Volo.Chat.Domain (via standard module deps)
      • Volo.Chat.SignalR
    • If using a gateway (YARP), route both REST and SignalR:
      • /api/chat/{**catch-all} -> your host
      • /signalr-hubs/{**catch-all} -> your host
  • Add module dependencies

    • Blazor Server UI module:
using Volo.Abp.Modularity;
    using Volo.Chat.Blazor;
    using Volo.Chat.SignalR;

    [DependsOn(
        typeof(ChatBlazorModule),
        typeof(ChatSignalRModule)
    )]
    public class YourAppBlazorModule : AbpModule
    {
    }
  • HttpApi.Host module:
using Volo.Abp.Modularity;
    using Volo.Chat;
    using Volo.Chat.SignalR;

    [DependsOn(
        typeof(ChatHttpApiModule),
        typeof(ChatApplicationModule),
        typeof(ChatSignalRModule)
    )]
    public class YourAppHttpApiHostModule : AbpModule
    {
    }
  • Enable the Chat feature (tenant/host)

    • Go to Administration -> Feature Management for the current tenant (or host if you’re on host) and enable the Chat feature. Without this, the UI won’t render the chat toolbar item.
    • After enabling, sign out and sign in again to refresh the application configuration.
  • Grant Chat permissions to the user/role

    • Grant Chat.Messaging (and other Chat permissions as needed) to the role of the logged-in user (e.g., admin) under Identity -> Roles -> Permissions.
    • If you changed permissions and still can’t see the UI, clear the cache (e.g., Redis) and re-login.
  • Ensure SignalR hub is reachable

    • Chat uses SignalR hub at /signalr-hubs/chat. Make sure:
      • app.MapHub<...> is configured by ChatSignalRModule (happens when you depend on it).
      • If behind YARP, forward:
        • /signalr-hubs/{**catch-all} to the same backend as your HttpApi.Host.
      • If you see 401 for the GET to /signalr-hubs/chat with access_token in query, forward the token to Authorization header in gateway:
app.Use(async (ctx, next) =>
      {
          var accessToken = ctx.Request.Query["access_token"];
          if (!string.IsNullOrEmpty(accessToken) &&
              ctx.Request.Path.StartsWithSegments("/signalr-hubs/chat"))
          {
              ctx.Request.Headers["Authorization"] = "Bearer " + accessToken;
          }
          await next();
      });
  • Blazor Server toolbar visibility

    • The chat toolbar item is added by the Chat Blazor package. If you customized the layout or toolbar rendering, ensure the toolbar component (NavToolbar) is present in your layout and the theme isn’t overriding/removing toolbar items.
    • Verify the menu/toolbar contributor isn’t filtered by tenant/feature/permission due to missing permission or disabled feature (see above).
  • Database/migration

    • You already ran the migrations; ensure the chat tables exist and no startup exceptions occur. Check logs for any Chat-related errors at startup.
  • Common pitfalls

    • Package added to wrong project (e.g., installed only to AuthServer or a microservice instead of the HttpApi.Host that serves your Blazor UI). Ensure the HttpApi.Host that your Blazor UI calls has ChatHttpApi + ChatSignalR.
    • Missing ChatSignalRModule dependency on the backend or missing route in gateway for /signalr-hubs.
    • Feature enabled on host but you are logged into a tenant (or vice versa). Enable where you log in.
    • Permissions not granted to the current role.
    • Cached config/permissions. Re-login and clear caches if needed.

If you complete the steps above, the chat icon should appear in the top toolbar and the API calls to /api/chat/... and SignalR to /signalr-hubs/chat should succeed.

  1. Sources
  • https://abp.io/docs/latest/modules/chat
  • https://abp.io/docs/latest/framework/real-time/signalr
  • https://abp.io/support/questions/6782
  • https://abp.io/support/questions/5007
  • https://abp.io/support/questions/4426
  • https://abp.io/docs/latest/framework/ui/blazor/navigation-menu

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.

Answer
  1. Solution To have three textboxes on the login page (Tenant name, Username, Password) and remove the tenant switch button for an Angular UI with LeptonX theme and multi-tenancy enabled, do the following:

A) Remove the tenant switch UI on the Account layout (server-side) for LeptonX

  • Create a view override to remove the built-in tenant switcher that appears on the public Account pages (Login, etc.).
  • In your AuthServer (or Public Web, depending on template), add this file with the same path to override the theme’s layout:
    • Path: Themes/LeptonX/Layouts/Account/Default.cshtml
  • Copy the original content from the LeptonX Account layout and remove (or comment out) the block that renders the tenant switch:
    • The block guarded by: if (MultiTenancyOptions.Value.IsEnabled && (TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(CookieTenantResolveContributor.ContributorName) == true || TenantResolveResultAccessor.Result?.AppliedResolvers?.Contains(QueryStringTenantResolveContributor.ContributorName) == true))
    • Delete that entire block (including the link with id="AbpTenantSwitchLink").
  • This ensures no switch button is shown on the login page.

B) Add a Tenant Name textbox to the Angular login component and send it with the login request

  • In the Angular application (apps/angular), extend the login form to include a Tenant Name field and set the tenant header (__tenant) programmatically. ABP Angular sends the current tenant on each request from the application-configuration result. Since you need a textbox on the login form, set the tenant before calling the login API.

Example steps:

  1. Create a form control and UI for tenant input:

    • If you are using the out-of-the-box LoginComponent from @abp/ng.account (Public), create a custom login component (or extend the existing one by wrapping it) that:
      • Adds a tenantName: FormControl to the form.
      • Renders a textbox above Username and Password.
  2. Set the tenant header before login:

    • Inject RestService or use ABP Angular’s tenant setter by writing the selected tenant name to the cookie/header before invoking login.
    • For Angular, ABP resolves tenant from:
      • __tenant header, or
      • abp.tenant cookie, or
      • query string (__tenant)
    • For a simple approach, set the abp.tenant cookie from the textbox value just before calling login; ABP’s standard backend resolves it.

Example (illustrative, place inside your custom login component):

import { Component, inject } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { RestService } from '@abp/ng.core';
import { AccountService } from '@abp/ng.account/public/config'; // or your own auth call

@Component({
  selector: 'app-custom-login',
  template: `
  &lt;form [formGroup]=&quot;form&quot; (ngSubmit)=&quot;login()&quot;&gt;
    &lt;label&gt;Tenant Name&lt;/label&gt;
    &lt;input formControlName=&quot;tenantName&quot; type=&quot;text&quot; autocomplete=&quot;organization&quot; /&gt;

    &lt;label&gt;Username&lt;/label&gt;
    &lt;input formControlName=&quot;username&quot; type=&quot;text&quot; autocomplete=&quot;username&quot; /&gt;

    &lt;label&gt;Password&lt;/label&gt;
    &lt;input formControlName=&quot;password&quot; type=&quot;password&quot; autocomplete=&quot;current-password&quot; /&gt;

    &lt;button type=&quot;submit&quot; [disabled]=&quot;form.invalid || submitting&quot;&gt;Login&lt;/button&gt;
  &lt;/form&gt;
  `
})
export class CustomLoginComponent {
  private fb = inject(FormBuilder);
  private rest = inject(RestService);
  private account = inject(AccountService); // if you use the ABP Account endpoints

  submitting = false;

  form = this.fb.group({
    tenantName: [''],
    username: ['', Validators.required],
    password: ['', Validators.required],
    rememberMe: [false],
  });

  private setTenantCookie(name: string | null | undefined) {
    // When empty, clear cookie; when not empty, set cookie.
    const cookieName = 'abp.tenant';
    const encoded = name ? encodeURIComponent(name) : '';
    const expires = new Date();
    expires.setDate(expires.getDate() + 30);
    if (encoded) {
      document.cookie = `${cookieName}=${encoded}; path=/; expires=${expires.toUTCString()}; SameSite=Lax`;
    } else {
      // clear
      document.cookie = `${cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`;
    }
  }

  async login() {
    if (this.form.invalid) return;
    this.submitting = true;
    try {
      const tenantName = (this.form.value.tenantName || '').trim();
      // Ensure the backend resolves the request under this tenant:
      this.setTenantCookie(tenantName || null);

      // If you call login via a custom HTTP request, you can also set __tenant header:
      // Example:
      // await this.rest.request({
      //   method: 'POST',
      //   url: '/api/account/login',
      //   body: { userName: this.form.value.username, password: this.form.value.password },
      //   headers: tenantName ? { __tenant: tenantName } : {}
      // }).toPromise();

      // If using ABP AccountService (OIDC flow), just trigger sign-in flow after setting cookie
      await this.account.navigateToLogin(); // or start the flow used in your app

    } finally {
      this.submitting = false;
    }
  }
}

Notes:

  • If you use the standard OIDC login redirection provided by the Account Public module (AuthServer), setting abp.tenant cookie before starting the login is enough for the server to pick the correct tenant.
  • If you are calling a username/password endpoint directly, add the __tenant header to that request instead of (or in addition to) the cookie.

C) Ensure tenant resolution is configured to accept cookie/header

  • ABP’s default multi-tenancy supports cookie and header resolvers. By default, Angular UI sends __tenant. If you rely on the cookie, ensure the CookieTenantResolveContributor is active (it is by default). If you previously customized AbpTenantResolveOptions to remove resolvers, re-include CookieTenantResolveContributor or HeaderTenantResolveContributor.

Example to ensure defaults (in your HttpApiHostModule or AuthServer):

using Volo.Abp.MultiTenancy;

public override void ConfigureServices(ServiceConfigurationContext context)
{
    Configure<AbpTenantResolveOptions>(options =>
    {
        // Keep default resolvers, or ensure header/cookie are present
        // options.TenantResolvers.Clear();
        // options.TenantResolvers.Add(new HeaderTenantResolveContributor());
        // options.TenantResolvers.Add(new CookieTenantResolveContributor());
        // options.TenantResolvers.Add(new QueryStringTenantResolveContributor());
    });
}

Outcome

  • Login page shows three textboxes: Tenant Name, Username, Password.
  • No tenant switch button is available on the page.
  • The chosen tenant is applied for the login request via cookie or header consistently with ABP multi-tenancy.
  1. Sources
  • https://abp.io/docs/latest/framework/ui/angular/multi-tenancy
  • https://abp.io/docs/latest/framework/architecture/multi-tenancy#host-or-tenant
  • https://abp.io/support/questions/9283

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.

  1. Solution
  • What they do

    • CheckTokenExpiration (used in older templates, e.g., ABP 7.x Blazor Server):
      • Reads the access token expiration time stored in the auth cookie properties (typically .Token.expires_at).
      • On each cookie validation (OnValidatePrincipal), it compares the stored expiration against current time (with an optional advance window) and signs the user out if the token is expired or about to expire.
      • It is a local check only. It does not contact the Authorization Server. If the access token was revoked early (before its exp claim), this method won’t detect it.
    • IntrospectAccessToken (used in newer templates, e.g., ABP 8.x Blazor Server):
      • On each cookie validation (OnValidatePrincipal), it fetches the access_token from the cookie properties and calls the OIDC/OAuth2 token introspection endpoint via the OpenIdConnect backchannel using TokenIntrospectionRequest.
      • If the response indicates the token is inactive/expired, it signs the user out.
      • This is a server-side, authoritative check. It detects early revocation or other invalidation even if the token’s exp hasn’t passed.
  • Why ABP moved from CheckTokenExpiration to IntrospectAccessToken for Blazor Server templates

    • To prevent “desynchronization” scenarios where the UI still considers the user authenticated while the token used to call downstream services is already invalid or has been revoked.
    • Introspection ensures the cookie session always reflects the current validity status of the access token on the Auth Server/OpenIddict.
  • When to use which

    • Use IntrospectAccessToken if your Auth Server supports RFC 7662 token introspection (OpenIddict/Auth Server in ABP does) and you need immediate revocation/blacklist awareness or multi-module/microservice consistency.
    • Use CheckTokenExpiration only if you can’t use introspection. It’s lighter but only checks the client-stored expiration timestamp and can miss revocations.
  • How to configure in Blazor Server (ABP 8.x)

public override void ConfigureServices(ServiceConfigurationContext context)
  {
      Configure<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme, options =>
      {
          // Preferred in v8.x:
          options.IntrospectAccessToken(oidcAuthenticationScheme: OpenIdConnectDefaults.AuthenticationScheme);

          // If you cannot introspect, fallback:
          // options.CheckTokenExpiration(); // optional: pass advance/validationInterval
      });
  }
  1. Sources
  • https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs
  • https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs
  • https://abp.io/support/questions/5071

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.

Showing 141 to 150 of 896 entries
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.2.0-preview. Updated on February 17, 2026, 09:10
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.