Activities of "maliming"

Hi,

Sorry to hear about the trouble. To narrow this down on 8.3.0 we'll need a bit more info from your side:

  1. Which background job provider are you using? The default Volo.Abp.BackgroundJobs store, Hangfire, Quartz, or RabbitMQ?
  2. Deployment topology — single instance or multi-instance? Hosted in IIS / Docker / Kubernetes / Azure App Service / etc.? Any recent infra changes around April 22 (database failover, Redis restart, scaling, network changes)?
  3. BackgroundJobOptions configuration — anything you've customized (e.g. IsJobExecutionEnabled, custom JobExecutionTimeout, DefaultTimeout)?
  4. State of the AbpBackgroundJobs table — a snapshot of a few stuck rows showing IsAbandoned, NextTryTime, TryCount, LastTryTime would help a lot.
  5. Application logs around April 22 — especially anything at Error / Warning level from Volo.Abp.BackgroundJobs.* or Volo.Abp.DistributedLocking.*.
  6. Debug logs — please enable Debug level logging for the background job namespaces and capture a few minutes of output while jobs are sitting in the queue. Steps here: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problemse

A few common causes we've seen for this exact symptom:

  • The worker host process is up but the background job worker isn't actually running (e.g. IsJobExecutionEnabled = false got set, or only the API host is running and the job host isn't).
  • A distributed lock (IAbpDistributedLock) is stuck — leftover lock row in DB / Redis that never expires, blocking the periodic queue poller.
  • Long-running DB transaction / blocking on the AbpBackgroundJobs table preventing the poller from reading the queue.
  • Job records all marked IsAbandoned = true after exceeding MaxTryCount (default 3), which is why "previously failing" jobs stopped retrying.

If you can share the logs and a sample of the AbpBackgroundJobs rows, we can dig in from there.

Thanks

Hi,

Yes, you can keep using AutoMapper. Starting from ABP 10.3.0, we shipped a new package Volo.Abp.LuckyPenny.AutoMapper exactly for this case — it's a drop-in replacement built on the commercial AutoMapper (15.x+) by LuckyPenny Software, which is the patched version (the free 14.x has a DoS vulnerability that won't be fixed).

Migration is just two steps in your own modules:

// *.csproj
-<PackageReference Include="Volo.Abp.AutoMapper" />
+<PackageReference Include="Volo.Abp.LuckyPenny.AutoMapper" />
// YourModule.cs
-[DependsOn(typeof(AbpAutoMapperModule))]
+[DependsOn(typeof(AbpLuckyPennyAutoMapperModule))]

All the types stay in the same namespaces (AbpAutoMapperOptions, IMapperAccessor, etc.), so your existing Profiles and CreateMap calls keep working as-is. License key goes through AbpAutoMapperOptions.Configurators:

Configure<AbpAutoMapperOptions>(options =>
{
    options.Configurators.Add(ctx =>
    {
        ctx.MapperConfiguration.LicenseKey = "YOUR_LICENSE_KEY";
    });
});

Note: Volo.Abp.LuckyPenny.AutoMapper and Volo.Abp.AutoMapper are mutually exclusive — pick one.

About Text Template Management (and other ABP modules migrated to Mapperly): you can use them just fine alongside AutoMapper in your own code. The Mapperly usage is internal to those modules — you don't need to write any Mapperly mappers yourself. The only thing you can't do is fully remove Mapperly from the solution, since the module assemblies are compiled against it. But it's just a transitive dependency, not something you have to interact with.

So the supported setup is:

  • Your application code → AutoMapper (via Volo.Abp.LuckyPenny.AutoMapper if you hold a commercial license)
  • ABP modules → Mapperly internally
  • Both coexist in the same solution without conflict

Docs: https://abp.io/docs/latest/framework/infrastructure/luckypenny-automapper

Thanks

Hi,

A couple of things to clear up first:

  • The "copy host admin and change TenantId" trick won't work — the tenant admin lives in the tenant DB (Isolated mode) and just changing TenantId on a host user breaks the multi-tenant integrity.
  • SwitchTenant grant only works when AbpMultiTenancyOptions.UserSharingStrategy == Shared. In Isolated mode the grant handler explicitly returns unsupported_grant_type, so adding it on the Angular OpenIddict client has no effect — please remove SwitchTenant from the Angular app's ExtensionGrantTypes.

For your "no SMTP allowed" scenario, the cleanest workaround is to replace the OOTB IIdentityEmailSender so the existing "Invite admin" button never calls SMTP. Instead, the accept-link is written to the log (or your own table / webhook / Redis — whatever your ops team can read). The host admin grabs the link from there and forwards it to the invitee through any internal channel (Slack/Teams/internal portal). The invitee opens the link, the OOTB Account/InviteUser page loads, they pick a username/password and accept.

Zero UI change, zero new endpoints, no client-proxy regeneration in layered solutions. The OOTB "Invite admin" button keeps working unchanged in MVC, Blazor, and Angular.

[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IIdentityEmailSender))]
public class NoSmtpIdentityEmailSender : IdentityEmailSender
{
    public NoSmtpIdentityEmailSender(
        IEmailSender emailSender,
        ITemplateRenderer templateRenderer,
        IStringLocalizer&lt;IdentityResource&gt; stringLocalizer,
        IAppUrlProvider appUrlProvider,
        UserSharingManager userSharingManager,
        UserInvitationManager userInvitationManager)
        : base(emailSender, templateRenderer, stringLocalizer, appUrlProvider,
               userSharingManager, userInvitationManager)
    {
    }

    public override async Task SendInviteEmailAsync(UserInvitation invitation, bool requireRegister, string appName)
    {
        var url = await AppUrlProvider.GetInviteUserUrlAsync(appName);
        var token = await UserInvitationManager.GenerateTokenFromInvitationIdAsync(invitation.Id);
        var link = $"{url}?token={UrlEncoder.Default.Encode(token)}";

        Logger.LogWarning(
            "[Invite] Tenant={tenantId} Email={email} RequireRegister={requireRegister} Link={link}",
            invitation.InviterTenantId, invitation.InviteeEmail, requireRegister, link);

        // Or persist to your own table / push to a webhook / write to Redis -
        // whatever your ops team can read from to retrieve the link.
    }
}

Drop this class into your Application module. Each invite produces a log line like:

[Invite] Tenant=... Email=admin@mycorp.com RequireRegister=True Link=https://your-app/Account/InviteUser?token=CfDJ8...

Just copy the Link=... value and forward it to the invitee. Done — no SMTP, no SwitchTenant, no host-user copying.

Thanks

Hi,

The "Switch Tenant" feature relies on a custom OpenIddict extension grant (SwitchTenant) plus a few /api/account/user-sharing endpoints. Here is everything you need to wire it up in Next.js.

1. Detect if Shared mode is enabled

Read multiTenancy.userSharingStrategy from the application configuration endpoint. Only show the "Switch tenant" UI when it equals 1 (Shared).

GET {HttpApiHost}/api/abp/application-configuration
{
  "multiTenancy": {
    "isEnabled": true,
    "userSharingStrategy": 1   // 0 = Isolated, 1 = Shared
  }
}

2. Get the list of tenants the current user is shared with

GET {HttpApiHost}/api/account/user-sharing
Authorization: Bearer <current_access_token>

Response:

{
  "items": [
    { "tenantId": "guid-or-null", "tenantName": "TenantA", "email": "user@x.com" },
    { "tenantId": "guid-or-null", "tenantName": "TenantB", "email": "user@x.com" }
  ]
}

3. Switch tenant via the custom SwitchTenant grant type

This is the key part. Post to the AuthServer token endpoint with grant_type=SwitchTenant. The current access_token is sent as a parameter (the grant handler introspects it to identify the user) and a new token set scoped to the target tenant is returned.

POST {AuthServerUrl}/connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=SwitchTenant
&client_id=<your_client_id>
&client_secret=<your_client_secret>      // only for confidential clients
&access_token=<current_valid_access_token>
&TenantId=<target_tenant_guid>           // omit / leave empty to switch to Host

Response is a standard OAuth token response:

{
  "access_token": "...",
  "refresh_token": "...",
  "id_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "..."
}

The new access_token already has the target tenant's tenantid claim, so every subsequent API call will run in the new tenant context.

After receiving the response in Next.js you should:

  • Replace the stored access_token / refresh_token / id_token
  • Reload the app (or re-initialize state) so all subsequent requests use the new tokens

This is exactly what Angular does — see SwitchTenantService in commercial-ui/config/src/services/switch-tenant.service.ts, which calls loginUsingGrant('SwitchTenant', { TenantId }) and then location.href = baseUrl to refresh.

4. Leave a shared tenant (optional)

DELETE {HttpApiHost}/api/account/user-sharing/leave?tenantId=<guid>
Authorization: Bearer <current_access_token>

5. Invitation endpoints (if you need invite/accept/reject in your UI)

GET  /api/account/user-sharing/invitation?token=<inviteToken>
POST /api/account/user-sharing/invitation/accept   // body: { token, ... }
POST /api/account/user-sharing/invitation/reject   // body: { token, ... }

Backend prerequisites (just to confirm)

Configure<AbpMultiTenancyOptions>(options =>
{
    options.IsEnabled = true;
    options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
});

If UserSharingStrategy is not Shared, the SwitchTenant grant returns unsupported_grant_type.

TL;DR for the Next.js side

  1. GET /api/abp/application-configuration → check multiTenancy.userSharingStrategy === 1
  2. GET /api/account/user-sharing → render the tenant list
  3. On selection: POST /connect/token with grant_type=SwitchTenant&access_token=<current>&TenantId=<guid>
  4. Save the new tokens and reload the app

Thanks

Hi @rcalv002,

Thanks for letting us know. We will continue working on the Elsa module, and any updates will be included in the release notes for upcoming versions.

Thanks

Hi,

Can you replace the contents of the inline workaround script block in your _Host.cshtml with this (the surrounding script tag and the rest of the page stay as-is):

// LeptonX 5.3.0 side-menu rebind workaround.
// initSideMenu runs once from SideMenuLayout.OnAfterRenderAsync and may
// execute against an empty #lpx-sidebar (when MainMenuProvider yields) or
// be skipped on impersonation rebuild. We watch the sidebar and, whenever
// an outer menu item ends up without click listeners, remove every
// previously attached listener (so we never double-bind a preserved
// element) and call initSideMenu once more to re-attach a single clean
// set. window.initLeptonX is wrapped so the natural call from
// OnAfterRenderAsync goes through the same cleanup path.
(function () {
    try {
        const allBinds = new WeakMap();
        let timer = null;

        const origAdd = EventTarget.prototype.addEventListener;
        EventTarget.prototype.addEventListener = function (type, fn, opts) {
            if (this instanceof Element
                && this.classList && this.classList.contains('lpx-menu-item')
                && this.closest && this.closest('#lpx-sidebar')) {
                let arr = allBinds.get(this);
                if (!arr) { arr = []; allBinds.set(this, arr); }
                arr.push({ type: type, fn: fn, opts: opts });
            }
            return origAdd.call(this, type, fn, opts);
        };

        function cleanupBinds() {
            document.querySelectorAll('#lpx-sidebar .lpx-menu-item').forEach(function (el) {
                const arr = allBinds.get(el);
                if (arr) {
                    arr.forEach(function (b) { el.removeEventListener(b.type, b.fn, b.opts); });
                    allBinds.delete(el);
                }
            });
        }

        const origInitLeptonX = window.initLeptonX;
        window.initLeptonX = function () {
            cleanupBinds();
            return origInitLeptonX.apply(this, arguments);
        };

        function reinit() {
            cleanupBinds();
            const fn = window.leptonx
                && window.leptonx.init
                && window.leptonx.init.initializers
                && window.leptonx.init.initializers.get('initSideMenu');
            if (fn) fn();
        }

        function check() {
            const items = document.querySelectorAll('#lpx-sidebar .outer-menu-item > .lpx-menu-item');
            if (items.length === 0) return;
            let needs = false;
            for (let i = 0; i < items.length; i++) {
                if (!allBinds.has(items[i])) { needs = true; break; }
            }
            if (needs) reinit();
        }

        function debounced() { clearTimeout(timer); timer = setTimeout(check, 300); }

        new MutationObserver(debounced).observe(document.body, { childList: true, subtree: true });
        debounced();
    } catch (err) {
        console.error('[lpx-fix] failed:', err);
    }
})();

A proper upstream fix is also queued for the next LeptonX patch release, so once you upgrade you can drop this whole inline script.

Thanks

Hi,

Thanks for the update, glad to hear you have an internal procedure in place for now. The fix is already merged and will ship in the next release, so you can drop the workaround once you upgrade.

Thanks

Answer

Hi,

Found the root cause. In your NotificationController.cs:

[HttpGet("/{id}")]
public Task<NotificationDto> GetAsync(Guid id)
{
    return _notificationAppService.GetAsync(id);
}

The leading / in [HttpGet("/{id}")] makes the route absolute, which overrides the controller-level [Route("api/app/notification")]. As a result this action matches any single-segment URL at the root — including /home, /about, etc.

Since Emerald.Web.Public depends on EmeraldHttpApiModule, this controller is registered in the Public website's routing table. When your HomeController redirects unauthenticated users to /home, ASP.NET Core matches NotificationController.GetAsync, tries to bind home to the Guid id parameter, fails, and returns 400. The [Authorize] attribute on the controller is also what triggers the AuthServer redirect you observed.

The log confirms this:

Executing endpoint 'Emerald.Controllers.Notifications.NotificationController.GetAsync (Emerald.HttpApi)'
Route matched with {area = "app", controller = "Notification", action = "Get"}. Executing controller action with signature ... GetAsync(System.Guid) ...
Request finished HTTP/2 GET https://localhost:44362/home - 400

Fix: Remove the leading / so the route becomes relative to the controller:

[HttpGet("{id}")]
public Task<NotificationDto> GetAsync(Guid id)
{
    return _notificationAppService.GetAsync(id);
}

The endpoint will then be GET /api/app/notification/{id} as intended.

Thanks

Hi,

Confirmed — with the default ABP setup you're running, your app isn't hit by CVE-2026-40372. ABP does not call UseDataProtection() on the OpenIddict server/validation builder, so all OpenIddict tokens (auth code, refresh, device code, state) are encrypted with the X.509 certificate you register via AddEncryptionCertificate(...), and access tokens are JWTs (DisableAccessTokenEncryption() is applied by default). The DP flaw only reaches the ASP.NET Core side of your app (cookies, antiforgery, ASP.NET Core Identity DP tokens), not OpenIddict tokens.

On top of that, Azure App Service Linux has already rolled the shared framework to 10.0.7+, which is what your framework-dependent hosts actually load for Microsoft.AspNetCore.DataProtection. So in practice you're already on the patched assembly.

Per-question answers below.

1. Transitive exposure through ABP packages

ABP core and Volo.Abp.OpenIddict.* don't pull Microsoft.AspNetCore.DataProtection as a NuGet reference on net10.0 — on that TFM, OpenIddict.*.AspNetCore gets DataProtection from the Microsoft.AspNetCore.App shared framework. The only NuGet-level DP refs in ABP templates are Microsoft.AspNetCore.DataProtection.StackExchangeRedis and, on the Pro side, a direct Microsoft.AspNetCore.DataProtection reference in Volo.Abp.Identity.Pro.Domain and Volo.FileManagement.Domain.

Loading rule is max(sharedFramework, PackageReference), and it's safe iff that max is ≥ 10.0.7. As long as your runtime patch is 10.0.7+, the shared framework wins and you're safe even with the current 10.0.2 PackageReference in ABP 10.2.

We already pushed PRs on April 23 to bump all Microsoft.* / System.* packages to 10.0.7 across ABP, ABP Pro, and ABP Studio, so you'll pick that up in the next 10.2.x / 10.3.x patch. If you want a hard floor now without waiting for the bump, add these to each host csproj — this guarantees NuGet wins even if the runtime patch somewhere lags:

<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.7" />

2. Recommended revocation pattern for tiered solutions

Your plan is exactly what we'd recommend: one-shot IHostedService on the AuthServer, gated by a config flag. The AuthServer owns the key-ring registration and the OpenIddict domain services, so both IKeyManager.RevokeAllKeys() and IOpenIddictTokenManager.RevokeAsync(null, null, null, null) belong there. ABP doesn't have a dedicated idiomatic hook for this; IHostedService or OnApplicationInitializationAsync both work. Use a DB row / Redis key as an idempotency marker so it doesn't re-run on every restart.

Sketch:

public class CveKeyRevocationHostedService : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;

    public CveKeyRevocationHostedService(IServiceProvider serviceProvider, IConfiguration configuration)
    {
        _serviceProvider = serviceProvider;
        _configuration = configuration;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        if (!_configuration.GetValue<bool>("CabMD:DataProtection:RevokeOnStartup"))
        {
            return;
        }

        using var scope = _serviceProvider.CreateScope();

        var keyManager = scope.ServiceProvider.GetRequiredService<IKeyManager>();
        keyManager.RevokeAllKeys(DateTimeOffset.UtcNow, "CVE-2026-40372");

        var tokenManager = scope.ServiceProvider.GetRequiredService<IOpenIddictTokenManager>();
        await tokenManager.RevokeAsync(subject: null, client: null, status: null, type: null);

        // persist an idempotency marker here (DB row / Redis key) so a later restart skips this
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Register it in the AuthServer module's ConfigureServices:

context.Services.AddHostedService<CveKeyRevocationHostedService>();

3. ABP-internal consumers of Data Protection

Here's the full list for an ABP tiered setup. Everything below is invalidated by RevokeAllKeys — but none of it stores DP-protected ciphertext in the database, so revocation only forces re-auth and invalidates pending email/share links. It does not break any at-rest data.

  • ASP.NET Core cookie authentication (session cookies across AuthServer / Web / Web.Public / Hangfire / HttpApi.Host)
  • ASP.NET Core antiforgery tokens
  • OIDC external login state parameter (Google/Microsoft/etc. handlers)
  • ASP.NET Core Identity DP-based token providers (ABP extends these):
    • email confirmation
    • password reset
    • change email / change phone number
    • link external login
    • AbpSingleActiveTokenProvider — stores a SHA256 hash in AbpUserTokens, not the DP ciphertext, so revoke is safe
  • ABP Pro only:
    • UserInvitationTokenProvider — pending invitation emails become invalid
    • ShareFileTokenProvider in File Management — any outstanding share link becomes invalid

Not affected: all OpenIddict tokens (auth code, refresh, device code, state, access token) — they use the OpenIddict encryption/signing certificate, not DP.

4. OpenIddict encryption/signing certificate vs. DP key ring

Correct — they're completely separate. The certificate registered via AddEncryptionCertificate / AddSigningCertificate on OpenIddictServerBuilder encrypts OpenIddict's own tokens. The DP key ring is only used by ASP.NET Core (cookies, antiforgery) and ASP.NET Core Identity DP token providers. CVE-2026-40372 is purely about the DP encryptor HMAC flaw, so you do not need to rotate the OpenIddict encryption/signing cert for this CVE.

5. SetApplicationName interop after revocation

There is a per-host cache. KeyRingProvider caches the active key ring, and KeyManagementOptions.KeyRingRefreshPeriod controls refresh (default 24h). RevokeAllKeys does not push a notification to other hosts — they'll only pick up the revocation and the new active key on their next refresh.

That means there is a transient window after you call RevokeAllKeys where the other hosts are still holding the stale key ring: unprotect will fail (keys revoked), and protect operations on those nodes can race when auto-generating a new active key.

Coordinated restart of all hosts (AuthServer + HttpApi.Host + Web + Web.Public + Hangfire) immediately after revocation is the right call. Don't rely on the 24h refresh for incident response.

Recommended sequence for your deployment window:

  1. Deploy all hosts with the 10.0.7 packages (and, if you want the PackageReference floor, the two lines from Q1).
  2. Flip CabMD:DataProtection:RevokeOnStartup=true on the AuthServer only.
  3. Restart the AuthServer — the hosted service calls RevokeAllKeys + RevokeAsync.
  4. Immediately restart the remaining hosts (HttpApi.Host / Web / Web.Public / Hangfire) so they flush their cached key ring.
  5. Flip the flag back to false and redeploy the AuthServer (or just rely on the idempotency marker).

Thanks.

Hi,

Thanks for catching this during testing — really appreciate it, that kind of feedback before production saves us a lot of pain. The fix will land in the next release, so you can grab it from there.

Thanks

Showing 571 to 580 of 12915 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.8.0-preview. Updated on November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.