Open Closed

Shared Accounts from Tenants #10604


User avatar
0
tony.gayter created

Hi,

I recently tested the shared user accounts and everything worked correctly on my local machine. However, once deployed to staging, the login stopped working and would just redirect back to the login page.

After investigating, I found that the affected accounts have 2FA enabled, and that appears to be causing the issue. The flow does not proceed to the 2FA screen and instead falls back to the login page.

Are there any known issues with shared accounts when 2FA is enabled?

I also tried deleting all user accounts and recreating them from scratch in case there was a setup issue, but that did not resolve the problem.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

16 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • With Shared User Accounts (TenantUserSharingStrategy.Shared), the user identity is global, so security settings like 2FA are evaluated at the Host side (not per-tenant). This is expected behavior and is how ABP is designed for shared accounts.
    • There’s no verified ABP source that says “2FA + shared accounts is broken” as a known product issue. When the app redirects back to the login page instead of continuing to the 2FA step, in ABP-based apps this is most commonly caused by an environment/deployment cookie/auth configuration problem (e.g., cookies not persisted/sent), not by the shared-accounts feature itself.

    What to do (staging fix checklist that typically resolves “login loops” specifically when 2FA is enabled):

    1. Make sure your cookies work correctly in staging (HTTPS + SameSite + proxy headers)
      • Ensure staging is running behind HTTPS and your ASP.NET Core app correctly detects HTTPS when behind a reverse proxy/load balancer.
      • Configure forwarded headers (common requirement in staging/production behind a proxy), otherwise auth cookies/redirect URIs can behave unexpectedly and cause loops:
    app.UseForwardedHeaders();
    
     (Plus the corresponding `ForwardedHeadersOptions` configuration if needed in your environment.)
    
    1. Verify Data Protection key persistence across instances/restarts

      • 2FA flows rely on temporary auth state. If you are running multiple instances (or the container/app restarts) and DataProtection keys are not shared/persisted, tokens/cookies used during the sign-in flow can become invalid and you can see redirects back to login.
      • Persist keys to a shared location (filesystem/Redis/Azure Blob, etc.) for staging/production.
    2. If you are using OpenIddict / separated AuthServer, ensure all auth-related hosts share consistent settings

      • Mismatched cookie domain, SameSite policy, or incorrect external/public URL configuration can show up more often when 2FA is enabled because the flow needs additional round-trips.
    3. Confirm 2FA configuration is done at the correct level

      • Since shared accounts treat identity/security as Host-level, ensure the relevant settings are configured consistently at Host (and not assumed to be tenant-scoped).

    No verified source available for a specific “shared accounts + 2FA causes redirect loop” known issue; the above items are the standard, proven causes in staging/production for “back to login page” loops during multi-step sign-in flows (like 2FA).

    • https://abp.io/docs/latest/modules/account/shared-user-accounts
    • https://abp.io/docs/latest/modules/identity/two-factor-authentication

    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.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Since you're on 10.2+, shared accounts should already ship with AbpAccountTenantResolveContributor that forces CurrentTenant = null during the 2FA flow (because the user is not yet fully authenticated when entering the 2FA code). So in theory the 2FA page should look up the host user and work fine.

    Before I dig deeper, a few questions to narrow it down — could you check and share:

    1. How do you resolve tenants? Especially: are you using domain / subdomain tenant resolution (e.g. tenant1.mydomain.com)? If yes, please share the related configuration (where you call options.Tenants, AddDomainTenantResolver, or anything that inserts a domain/subdomain contributor). If you inserted it before CurrentUserTenantResolveContributor, that will bypass the shared-mode guard above and CurrentTenant will be set to the tenant on the 2FA page, which breaks GetTwoFactorAuthenticationUserAsync.

    2. Confirm UserSharingStrategy — please share your Configure<AbpMultiTenancyOptions>(...) block from staging. Staging and local may not be on the same configuration:

    Configure<AbpMultiTenancyOptions>(options =>
    {
        options.IsEnabled = true;
        options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
    });
    
    1. What is the login URL on staging? Is it the host domain, a tenant subdomain, or a dedicated account domain? And what about local?

    2. Browser DevTools check — reproduce the issue on staging, then in DevTools → Application → Cookies, check:

      • Is the .AspNetCore.Identity.TwoFactorUserId cookie written after you submit username/password?
      • When the browser follows the redirect to /Account/SendSecurityCode, is that cookie sent back in the request headers?
      • Any relevant Domain / SameSite / Secure differences vs. local?
    3. Debug logs from the failing request on staging — please temporarily enable Debug level logs and capture the logs from the moment you submit the login form until you get redirected back to the login page. See this guide: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems

      • We're specifically looking for the CurrentTenant.Id values, any CheckCurrentTenant failures (e.g. ApplicationException: Current tenant is different than given tenant. CurrentTenant.Id: ..., given tenantId: ...), SignInManager/TwoFactor related log entries, and any warnings/exceptions around the redirect.
    4. Does this happen for every 2FA account on staging, or only some of them? And does a pure host user (not in any tenant) with 2FA work on staging?

    My current suspicion is either (a) a domain/subdomain tenant resolver inserted before the shared-mode guard, or (b) a cookie issue (SameSite/Domain) on staging that loses the TwoFactorUserId cookie across the redirect. The debug logs + the answers above should tell us which one.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created
    1. We are just using the standard tenant resolution, previously its was the 'Switch Tenant' modal which was a bit messy (hence the desire to move over).
    2. This is the setup, really simple.
    public class DCOTDomainModule : AbpModule
    {
        public override void ConfigureServices(ServiceConfigurationContext context)
        {
            Configure<AbpMultiTenancyOptions>(options =>
            {
                options.IsEnabled = MultiTenancyConsts.IsEnabled;
                options.UserSharingStrategy = TenantUserSharingStrategy.Shared;
            });
        }
    }
    
    1. The login url is on the main domain, there is no separation. Just goes to the domain, and login. https://dcot-cms.bbd.agency/Account/Login?ReturnUrl=%2F``

    This happens on both staging and live (and local when 2fa is enabled). From the logs, it looks like it's failing on authoization? We have a policy for JWT on our API but it's only applied to our API controllers.

    Also, It wont let me attach files to this comment for some reason? if i drag and drop, it opens in a new window? I have pasted in the area of the log file where it says about the auth failure

    2026-04-20 10:41:47.323 +01:00 [DBG] No tenant resolved. 2026-04-20 10:41:47.323 +01:00 [INF] Executing endpoint 'Volo.Abp.AspNetCore.Mvc.Localization.AbpApplicationLocalizationScriptController.GetAsync (Volo.Abp.AspNetCore.Mvc)' 2026-04-20 10:41:47.323 +01:00 [INF] Executing endpoint 'Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationScriptController.Get (Volo.Abp.AspNetCore.Mvc)' 2026-04-20 10:41:47.323 +01:00 [INF] Route matched with {area = "Abp", action = "GetAll", controller = "AbpServiceProxyScript", page = ""}. Executing controller action with signature System.Threading.Tasks.Task1[Microsoft.AspNetCore.Mvc.ActionResult] GetAll(Volo.Abp.AspNetCore.Mvc.ProxyScripting.ServiceProxyGenerationModel) on controller Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController (Volo.Abp.AspNetCore.Mvc). 2026-04-20 10:41:47.324 +01:00 [INF] Route matched with {area = "Abp", action = "Get", controller = "AbpApplicationLocalizationScript", page = ""}. Executing controller action with signature System.Threading.Tasks.Task1[Microsoft.AspNetCore.Mvc.ActionResult] GetAsync(Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.ApplicationLocalizationRequestDto) on controller Volo.Abp.AspNetCore.Mvc.Localization.AbpApplicationLocalizationScriptController (Volo.Abp.AspNetCore.Mvc). 2026-04-20 10:41:47.324 +01:00 [INF] Route matched with {area = "Abp", action = "Get", controller = "AbpApplicationConfigurationScript", page = ""}. Executing controller action with signature System.Threading.Tasks.Task`1[Microsoft.AspNetCore.Mvc.ActionResult] Get() on controller Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationScriptController (Volo.Abp.AspNetCore.Mvc). 2026-04-20 10:41:47.324 +01:00 [DBG] Starting resolving tenant... 2026-04-20 10:41:47.324 +01:00 [DBG] Trying to resolve tenant through 'CurrentUser'... 2026-04-20 10:41:47.324 +01:00 [DBG] Trying to resolve tenant through 'AbpAccount'... 2026-04-20 10:41:47.324 +01:00 [DBG] Tenant resolved by 'AbpAccount' as 'Host'. 2026-04-20 10:41:47.324 +01:00 [DBG] No tenant resolved. 2026-04-20 10:41:47.324 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [DBG] Starting resolving tenant... 2026-04-20 10:41:47.324 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [DBG] Trying to resolve tenant through 'CurrentUser'... 2026-04-20 10:41:47.324 +01:00 [DBG] Trying to resolve tenant through 'AbpAccount'... 2026-04-20 10:41:47.324 +01:00 [DBG] Tenant resolved by 'AbpAccount' as 'Host'. 2026-04-20 10:41:47.324 +01:00 [DBG] No tenant resolved. 2026-04-20 10:41:47.324 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [INF] Sending file. Request path: 'libs/abp/luxon/abp.luxon.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\c1lbzjgcmt-{0}-z5wqywdzu2-z5wqywdzu2.gz' 2026-04-20 10:41:47.324 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [INF] Sending file. Request path: 'libs/bootstrap-daterangepicker/daterangepicker.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\57rtwhjdfx-{0}-gmkpl5fspb-gmkpl5fspb.gz' 2026-04-20 10:41:47.324 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.324 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/abp/luxon/abp.luxon.js?_v=639118456303580000 - 200 3423 text/javascript 7.391ms 2026-04-20 10:41:47.324 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.325 +01:00 [INF] Sending file. Request path: 'libs/jquery-validation/jquery.validate.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\jx337kzfp1-{0}-lzl9nlhx6b-lzl9nlhx6b.gz' 2026-04-20 10:41:47.325 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.325 +01:00 [INF] Executing endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.325 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/bootstrap-daterangepicker/daterangepicker.js?_v=639118456296730000 - 200 67788 text/javascript 6.28ms 2026-04-20 10:41:47.325 +01:00 [INF] Sending file. Request path: 'libs/bootstrap-datepicker/bootstrap-datepicker.min.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\zarr8osvcu-{0}-tiww64l1hz-tiww64l1hz.gz' 2026-04-20 10:41:47.325 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.325 +01:00 [INF] Sending file. Request path: 'libs/jquery-validation/abp.jquery.validate.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\9ibnrclyw3-{0}-2j57wpbqm1-2j57wpbqm1.gz' 2026-04-20 10:41:47.325 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.325 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/jquery-validation/jquery.validate.js?_v=639063219494640000 - 200 52574 text/javascript 8.7879ms 2026-04-20 10:41:47.325 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/jquery-validation/abp.jquery.validate.js?_v=639118456303560000 - 200 1112 text/javascript 9.0033ms 2026-04-20 10:41:47.325 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/bootstrap-datepicker/bootstrap-datepicker.min.js?_v=639063219497510000 - 200 33910 text/javascript 7.2086ms 2026-04-20 10:41:47.325 +01:00 [INF] Sending file. Request path: 'libs/luxon/luxon.min.js'. Physical path: 'C:\Dev\Code\Visa\WebsiteAuditorMain\src\BBD.DCOT.Blazor\obj\Debug\net10.0\compressed\p4w56pgbzy-{0}-z7d3m257il-z7d3m257il.gz' 2026-04-20 10:41:47.325 +01:00 [INF] Executed endpoint 'Microsoft.AspNetCore.Routing.RouteEndpoint' 2026-04-20 10:41:47.326 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/libs/luxon/luxon.min.js?_v=639063219498910000 - 200 81737 text/javascript 8.5416ms 2026-04-20 10:41:47.326 +01:00 [INF] Executing action method Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController.GetAll (Volo.Abp.AspNetCore.Mvc) - Validation state: "Valid" 2026-04-20 10:41:47.326 +01:00 [INF] Executing action method Volo.Abp.AspNetCore.Mvc.Localization.AbpApplicationLocalizationScriptController.GetAsync (Volo.Abp.AspNetCore.Mvc) - Validation state: "Valid" 2026-04-20 10:41:47.326 +01:00 [INF] Executed action method Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController.GetAll (Volo.Abp.AspNetCore.Mvc), returned result Microsoft.AspNetCore.Mvc.ContentResult in 0.3247ms. 2026-04-20 10:41:47.326 +01:00 [INF] Executing ContentResult with HTTP Response ContentType of application/javascript 2026-04-20 10:41:47.326 +01:00 [INF] Executed action Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController.GetAll (Volo.Abp.AspNetCore.Mvc) in 2.9371ms 2026-04-20 10:41:47.326 +01:00 [INF] Executed endpoint 'Volo.Abp.AspNetCore.Mvc.ProxyScripting.AbpServiceProxyScriptController.GetAll (Volo.Abp.AspNetCore.Mvc)' 2026-04-20 10:41:47.328 +01:00 [INF] Executing action method Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations.AbpApplicationConfigurationScriptController.Get (Volo.Abp.AspNetCore.Mvc) - Validation state: "Valid" 2026-04-20 10:41:47.328 +01:00 [INF] Request finished HTTP/2 GET https://localhost:44344/Abp/ServiceProxyScript - 200 136835 application/javascript 8.8329ms 2026-04-20 10:41:47.330 +01:00 [DBG] Executing AbpApplicationConfigurationAppService.GetAsync()... 2026-04-20 10:41:47.331 +01:00 [INF] Authorization failed. These requirements were not met: ClaimsAuthorizationRequirement:Claim.Type=DCOT.AttestationAlerts and Claim.Value is one of the following values: (true) ClaimsAuthorizationRequirement:Claim.Type=DCOT.AttestationAlerts.Edit and Claim.Value is one of the following values: (true) ClaimsAuthorizationRequirement:Claim.Type=DCOT.PageSnapshots and Claim.Value is one of the following values: (true) 2026-04-20 10:41:47.332 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessRequestContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ResolveRequestUri. 2026-04-20 10:41:47.332 +01:00 [DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers+ResolveRequestUri. 2026-04-20 10:41:47.332 +01:00 [DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by OpenIddict.Server.OpenIddictServerHandlers+InferEndpointType. 2026-04-20 10:41:47.332 +01:00 [DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by Volo.Abp.Account.Web.Pages.Account.OpenIddictImpersonateInferEndpointType. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Server.OpenIddictServerEvents+ProcessRequestContext was successfully processed by OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlers+ValidateHostHeader. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ValidateHostHeader. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.OpenIddictValidationHandlers+EvaluateValidatedTokens. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ExtractAccessTokenFromAuthorizationHeader. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ExtractAccessTokenFromBodyForm. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ExtractAccessTokenFromQueryString. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreHandlers+ExtractClientCertificate. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was successfully processed by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateRequiredTokens. 2026-04-20 10:41:47.333 +01:00 [DBG] The event OpenIddict.Validation.OpenIddictValidationEvents+ProcessAuthenticationContext was marked as rejected by OpenIddict.Validation.OpenIddictValidationHandlers+ValidateRequiredTokens. 2026-04-20 10:41:47.333 +01:00 [DBG] AuthenticationScheme: OpenIddict.Validation.AspNetCore was not authenticated. 2026-04-20 10:41:47.334 +01:00 [DBG] Starting resolving tenant... 2026-04-20 10:41:47.334 +01:00 [DBG] Trying to resolve tenant through 'CurrentUser'...

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    At the moment the multitenant is all within the same db as well (no db content switching)

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    One quick clarification on the Authorization failed lines you pasted earlier — those are not coming from ABP's standard permission system.

    ABP's permission checks go through PermissionRequirement (created by AbpAuthorizationPolicyProvider), and a failing ABP permission check would log as:

    Authorization failed. These requirements were not met:
    PermissionRequirement: DCOT.AttestationAlerts
    

    What you're seeing is:

    ClaimsAuthorizationRequirement:Claim.Type=DCOT.AttestationAlerts and Claim.Value is one of the following values: (true)
    

    ClaimsAuthorizationRequirement is ASP.NET Core's built-in type produced by policy.RequireClaim(...). It doesn't come from ABP — it's produced by hand-written AddAuthorization code somewhere in your solution.

    The DCOT.AttestationAlerts / DCOT.AttestationAlerts.Edit / DCOT.PageSnapshots names are almost certainly ABP permission names (ABP writes granted permissions into the principal as claims — claim type = permission name, value = "true" — via IAbpClaimsPrincipalContributor). But the fact that they're checked with ClaimsAuthorizationRequirement (all three in one policy) tells us there's a custom policy built like this somewhere in your code:

    services.AddAuthorization(options =>
    {
        options.AddPolicy("SomePolicyName", policy =>
            policy.RequireClaim("DCOT.AttestationAlerts", "true")
                  .RequireClaim("DCOT.AttestationAlerts.Edit", "true")
                  .RequireClaim("DCOT.PageSnapshots", "true"));
    
        // If the same policy is also assigned as the fallback, every endpoint without
        // an explicit [AllowAnonymous] will go through it — including /Account/SendSecurityCode
        options.FallbackPolicy = options.GetPolicy("SomePolicyName");
    });
    

    If that policy is set as FallbackPolicy / DefaultPolicy, or applied via Razor Pages conventions (AuthorizeFolder / AuthorizePage), it will kick in on /Account/SendSecurityCode too. A 2FA-mid-flow user only holds the TwoFactorUserIdScheme cookie, which does not carry those DCOT.* claims — so the page gets rejected and ASP.NET Core redirects the browser back to /Account/Login. That's a perfect match for your symptom (login → straight back to login, no 2FA screen), and it also explains why non-2FA accounts work (they get the full application cookie with all claims in a single POST, so they pass the fallback policy on subsequent requests).

    Could you please search your solution for:

    • Any services.AddAuthorization(options => ...) or Configure<AuthorizationOptions>(options => ...) blocks — especially anything setting FallbackPolicy or DefaultPolicy.
    • Any RequireClaim("DCOT.AttestationAlerts"...), RequireClaim("DCOT.PageSnapshots"...) usages.
    • Any Configure<RazorPagesOptions>(options => options.Conventions.AuthorizeFolder(...)) / AuthorizePage(...) that pulls in that policy.

    Share the snippets you find and we'll pinpoint the fix. If it is a fallback policy, the fix is usually either to scope the policy to specific endpoints (e.g. your API controllers via [Authorize("SomePolicyName")]) instead of using FallbackPolicy, or to explicitly allow /Account/* through.

    Alongside the config, the full debug log for one failing login attempt from POST /Account/Login until the browser ends up back on /Account/Login is still the other thing we really need — the snippet you sent before only covered the login page rendering (GETs for JS files), not the actual POST + redirect chain.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    yes so this is the auth code and policy. These should only be applied for api acccess

    private void ConfigureAuthorization(ServiceConfigurationContext context)
    {
        //Register authorization policy
        context.Services.AddAuthorization(options =>
        {
            options.AddPolicy(DCOTPolicies.Attestations.ApiAccess, policy =>
            {
                 policy.RequireClaim(DCOTPermissions.AttestationAlerts.Default, "true");
                 policy.RequireClaim(DCOTPermissions.AttestationAlerts.Edit, "true");
                 policy.RequireClaim(DCOTPermissions.PageSnapshots.Default, "true");
            });
        });
    }
    

    There are only two places it's applied, and they are on the endpoints themselves

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    It wont let me paste in a link to the txt file or zip file, it lets me for images though?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Thanks for the screenshots — checked them, that DCOTPolicies.Attestations.ApiAccess policy is unrelated to the 2FA redirect.

    To move forward, could you share two things:

    1. Full debug logs of one failing 2FA login(reproduce the problem).
    2. A Chrome HAR file capturing all requests/responses from clicking Login until the browser lands back on the login page

    Please follow this guide to enable all debug levels and HAR file: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problems

    Since the support form won't let you attach txt/zip, please email both files to liming.ma@volosoft.com and just reply here saying you've sent them.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    I have sent over the logs for you :)

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Got your log and HAR, thanks. The good news is this isn't a 2FA bug — the login never actually reaches the 2FA step. The log shows a plain password mismatch for user tony:

    [INF] Executing handler method Volo.Abp.Account.Public.Web.Pages.Account.LoginModel.OnPostAsync
    ...
    [DBG] Invalid password for user.
    [INF] Executed handler method OnPostAsync, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
    [INF] Request finished HTTP/2 POST .../Account/Login - 200 null text/html
    

    The HAR confirms it from the browser side:

    • Only one request to /Account/* in the whole capture — POST /Account/Login returning 200 (no redirect, no Location header).
    • No request to /Account/SendSecurityCode at all.
    • No .AspNetCore.Identity.TwoFactorUserId cookie is ever set in any response.
    • The response HTML contains the standard error alert rendered by the Login page:
    <div role="alert" class="alert alert-danger alert-dismissible fade show">
        Invalid username or password!
    </div>
    

    So what you're seeing as "redirect back to login" is actually the Login page re-rendering itself with the Invalid username or password alert after SignInManager.PasswordSignInAsync returned SignInResult.Failed. The RequiresTwoFactor branch is never hit, which is why you never see the 2FA screen.

    My guess on why this correlates with 2FA-enabled accounts: shared-mode user lookup prefers the host user when multiple matches exist. If your 2FA-enabled users were created/migrated in a way that left a different password hash on the host row vs the tenant row, the shared-mode login will now check against the host row — and the password you remember (set on the tenant row) won't match.

    Could you try the following and let me know:

    1. Sign in as an admin and reset tony's password from the host Identity Users page (make sure you're viewing Host users, not a tenant). Then try logging in with the new password.
    2. If that works and you land on the 2FA screen, we've confirmed it's a stale host password from the shared-mode migration.
    3. If it still fails after a host-side password reset, send me the new debug log + HAR of that attempt and we'll dig further.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Thanks for the log and HAR — with the password correct this time, the flow tells the full story:

    [INF] AuthenticationScheme: Identity.TwoFactorUserId signed in.          ← login wrote the 2FA cookie
    [INF] Executed handler method OnPostAsync, returned RedirectToPageResult
    [INF] Redirecting to ./SendSecurityCode.
    [INF] POST /Account/Login - 302
    
    [INF] Executing handler method SendSecurityCodeModel.OnGetAsync
    [INF] Executed handler method OnGetAsync, returned RedirectToPageResult  ← bounced back
    [INF] Redirecting to ./Login.
    [INF] GET /Account/SendSecurityCode - 302
    

    We've reproduced and confirmed this is a real bug in Account.Pro when Shared mode is combined with 2FA. Here's exactly what happens:

    1. On login, FindSharedUserByNameAsync locates the user across tenants. If the user row only lives under a tenant (no TenantId IS NULL row in AbpUsers), it returns the tenant user. The password check succeeds, 2FA is required, and ASP.NET Core Identity writes the .AspNetCore.Identity.TwoFactorUserId cookie. That cookie, by design of AspNetCore Identity, only carries the UserId — no TenantId.
    2. On the follow-up GET /Account/SendSecurityCode the user isn't authenticated yet, so AbpAccountTenantResolveContributor (Shared mode) short-circuits CurrentTenant to null.
    3. SendSecurityCode.OnGetAsync calls SignInManager.GetTwoFactorAuthenticationUserAsync(), which internally calls UserManager.FindByIdAsync(userId). That call goes through the default EF IMultiTenant filter (WHERE TenantId IS NULL), so the tenant-owned user row is filtered out, the method returns null, and the page redirects back to /Account/Login.

    Non-2FA accounts don't hit this because they complete the login in a single POST and get the full Identity.Application cookie (which does carry TenantId), so no tenant context is ever lost.

    We'll fix this in 10.2.2. In the meantime, as a drop-in workaround on your side, override the two affected Razor Pages in your Blazor host project. The overrides disable the IMultiTenant filter while fetching the 2FA user, then switch CurrentTenant to the user's tenant for the rest of the handler. I compiled both files against the 10.2 Volo.Abp.Account.Pro.Public.Web assembly to make sure they build cleanly:

    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Rendering;
    using Volo.Abp.Account;
    using Volo.Abp.Account.Public.Web.Pages.Account;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.Uow;
    
    namespace YourProject.Web.Pages.Account;
    
    [ExposeServices(typeof(SendSecurityCodeModel))]
    public class CustomSendSecurityCodeModel : SendSecurityCodeModel
    {
        [UnitOfWork]
        public override async Task<IActionResult> OnGetAsync()
        {
            var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
    
            Volo.Abp.Identity.IdentityUser user;
            using (dataFilter.Disable<IMultiTenant>())
            {
                user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
            }
    
            if (user == null)
            {
                return RedirectToPage("./Login");
            }
    
            using (CurrentTenant.Change(user.TenantId))
            {
                Providers = (await AccountAppService.GetTwoFactorProvidersAsync(new GetTwoFactorProvidersInput
                {
                    UserId = user.Id,
                    Token = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider,
                        nameof(Microsoft.AspNetCore.Identity.SignInResult.RequiresTwoFactor))
                })).Select(userProvider => new SelectListItem
                {
                    Text = userProvider,
                    Value = userProvider
                }).ToList();
            }
    
            return Page();
        }
    
        [UnitOfWork]
        public override async Task<IActionResult> OnPostAsync()
        {
            var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
    
            Volo.Abp.Identity.IdentityUser user;
            using (dataFilter.Disable<IMultiTenant>())
            {
                user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
            }
    
            if (user == null)
            {
                return RedirectToAction("Login");
            }
    
            using (CurrentTenant.Change(user.TenantId))
            {
                return await base.OnPostAsync();
            }
        }
    }
    
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Volo.Abp;
    using Volo.Abp.Account.Public.Web.Pages.Account;
    using Volo.Abp.Data;
    using Volo.Abp.DependencyInjection;
    using Volo.Abp.MultiTenancy;
    using Volo.Abp.Security.Claims;
    using Volo.Abp.Uow;
    
    namespace YourProject.Web.Pages.Account;
    
    [ExposeServices(typeof(VerifySecurityCodeModel))]
    public class CustomVerifySecurityCodeModel : VerifySecurityCodeModel
    {
        public CustomVerifySecurityCodeModel(ICurrentPrincipalAccessor currentPrincipalAccessor)
            : base(currentPrincipalAccessor)
        {
        }
    
        [UnitOfWork]
        public override async Task OnGetAsync()
        {
            var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
    
            Volo.Abp.Identity.IdentityUser user;
            using (dataFilter.Disable<IMultiTenant>())
            {
                user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
            }
    
            if (user == null)
            {
                throw new UserFriendlyException(L["VerifySecurityCodeNotLoggedInErrorMessage"]);
            }
    
            using (CurrentTenant.Change(user.TenantId))
            {
                IsRememberBrowserEnabled = await IsRememberBrowserEnabledAsync();
            }
        }
    
        [UnitOfWork]
        public override async Task<IActionResult> OnPostAsync()
        {
            var dataFilter = LazyServiceProvider.LazyGetRequiredService<IDataFilter>();
    
            Volo.Abp.Identity.IdentityUser user;
            using (dataFilter.Disable<IMultiTenant>())
            {
                user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
            }
    
            if (user == null)
            {
                return RedirectToPage("./Login");
            }
    
            using (CurrentTenant.Change(user.TenantId))
            {
                return await base.OnPostAsync();
            }
        }
    }
    

    Drop both files into your Blazor host project (any namespace is fine, the [ExposeServices] attribute tells ABP's DI to replace the built-in pages). No Startup / Module changes needed. Please give it a try and let me know if 2FA login works end-to-end for you.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    Thats great, thanks. All working and the code flows makes sense. Glad to have helped find a bug :)

    The only other thing would be a way to administer users from the host login, as I don't seem to be able to find a way to delete/disable a user from a tenant. Logging into a tenant seems the only way to see users, but it won't let me disable them? Not a big problem for now, so I will hopefully see something in a future release

    Appreciate the quick turn around in resolving this :)

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Great to hear the workaround is working for you. The fix is queued for 10.2.2, so once you move to that version you can drop the two custom pages.

    On managing tenant users from the host: right now the Users page is always scoped to the current tenant, so you have to be signed in as a tenant admin to edit / disable / delete users of that tenant.

    Thanks again for the detailed logs and HAR on the 2FA issue — that helped a lot.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Your current question has been refunded.

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    tony.gayter created

    Thank you, although there might be something wrong with the user stuff as well then because Im logged in as admin for the tenant (into the tenant) but am unable to delete or deactivate a user.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    That's actually intentional. In shared mode, users are global across host and all tenants, so things like activate / deactivate / lock / 2FA / delete only make sense at the host level. That's why the tenant admin UI hides those buttons — it's not a permission issue on your side.

    To remove someone from just your tenant, they can Leave from their own account menu. A full delete or deactivate needs a host admin to do it.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
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 September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.