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.
16 Answer(s)
-
0
- 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):
- 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.)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.
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.
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) - With Shared User Accounts (
-
0
Hi,
Since you're on 10.2+, shared accounts should already ship with
AbpAccountTenantResolveContributorthat forcesCurrentTenant = nullduring 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:
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 calloptions.Tenants,AddDomainTenantResolver, or anything that inserts a domain/subdomain contributor). If you inserted it beforeCurrentUserTenantResolveContributor, that will bypass the shared-mode guard above andCurrentTenantwill be set to the tenant on the 2FA page, which breaksGetTwoFactorAuthenticationUserAsync.Confirm
UserSharingStrategy— please share yourConfigure<AbpMultiTenancyOptions>(...)block from staging. Staging and local may not be on the same configuration:
Configure<AbpMultiTenancyOptions>(options => { options.IsEnabled = true; options.UserSharingStrategy = TenantUserSharingStrategy.Shared; });What is the login URL on staging? Is it the host domain, a tenant subdomain, or a dedicated account domain? And what about local?
Browser DevTools check — reproduce the issue on staging, then in DevTools → Application → Cookies, check:
- Is the
.AspNetCore.Identity.TwoFactorUserIdcookie 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/Securedifferences vs. local?
- Is the
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.Idvalues, anyCheckCurrentTenantfailures (e.g.ApplicationException: Current tenant is different than given tenant. CurrentTenant.Id: ..., given tenantId: ...),SignInManager/TwoFactorrelated log entries, and any warnings/exceptions around the redirect.
- We're specifically looking for the
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
TwoFactorUserIdcookie 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) -
0
- 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).
- 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; }); } }- 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.Task
1[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) -
0
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) -
0
Hi,
One quick clarification on the
Authorization failedlines you pasted earlier — those are not coming from ABP's standard permission system.ABP's permission checks go through
PermissionRequirement(created byAbpAuthorizationPolicyProvider), and a failing ABP permission check would log as:Authorization failed. These requirements were not met: PermissionRequirement: DCOT.AttestationAlertsWhat you're seeing is:
ClaimsAuthorizationRequirement:Claim.Type=DCOT.AttestationAlerts and Claim.Value is one of the following values: (true)ClaimsAuthorizationRequirementis ASP.NET Core's built-in type produced bypolicy.RequireClaim(...). It doesn't come from ABP — it's produced by hand-writtenAddAuthorizationcode somewhere in your solution.The
DCOT.AttestationAlerts/DCOT.AttestationAlerts.Edit/DCOT.PageSnapshotsnames are almost certainly ABP permission names (ABP writes granted permissions into the principal as claims — claim type = permission name, value ="true"— viaIAbpClaimsPrincipalContributor). But the fact that they're checked withClaimsAuthorizationRequirement(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/SendSecurityCodetoo. A 2FA-mid-flow user only holds theTwoFactorUserIdSchemecookie, which does not carry thoseDCOT.*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 => ...)orConfigure<AuthorizationOptions>(options => ...)blocks — especially anything settingFallbackPolicyorDefaultPolicy. - 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 usingFallbackPolicy, or to explicitly allow/Account/*through.Alongside the config, the full debug log for one failing login attempt from
POST /Account/Loginuntil the browser ends up back on/Account/Loginis 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) - Any
-
0
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) -
0
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) -
0
Hi,
Thanks for the screenshots — checked them, that
DCOTPolicies.Attestations.ApiAccesspolicy is unrelated to the 2FA redirect.To move forward, could you share two things:
- Full debug logs of one failing 2FA login(reproduce the problem).
- 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.comand 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) -
0
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) -
0
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/htmlThe HAR confirms it from the browser side:
- Only one request to
/Account/*in the whole capture —POST /Account/Loginreturning200(no redirect, noLocationheader). - No request to
/Account/SendSecurityCodeat all. - No
.AspNetCore.Identity.TwoFactorUserIdcookie 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 passwordalert afterSignInManager.PasswordSignInAsyncreturnedSignInResult.Failed. TheRequiresTwoFactorbranch 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:
- 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. - If that works and you land on the 2FA screen, we've confirmed it's a stale host password from the shared-mode migration.
- 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) - Only one request to
-
0
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 - 302We've reproduced and confirmed this is a real bug in
Account.Prowhen Shared mode is combined with 2FA. Here's exactly what happens:- On login,
FindSharedUserByNameAsynclocates the user across tenants. If the user row only lives under a tenant (noTenantId IS NULLrow inAbpUsers), it returns the tenant user. The password check succeeds, 2FA is required, and ASP.NET Core Identity writes the.AspNetCore.Identity.TwoFactorUserIdcookie. That cookie, by design of AspNetCore Identity, only carries the UserId — no TenantId. - On the follow-up
GET /Account/SendSecurityCodethe user isn't authenticated yet, soAbpAccountTenantResolveContributor(Shared mode) short-circuitsCurrentTenanttonull. SendSecurityCode.OnGetAsynccallsSignInManager.GetTwoFactorAuthenticationUserAsync(), which internally callsUserManager.FindByIdAsync(userId). That call goes through the default EFIMultiTenantfilter (WHERE TenantId IS NULL), so the tenant-owned user row is filtered out, the method returnsnull, 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.Applicationcookie (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
IMultiTenantfilter while fetching the 2FA user, then switchCurrentTenantto the user's tenant for the rest of the handler. I compiled both files against the 10.2Volo.Abp.Account.Pro.Public.Webassembly 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). NoStartup/Modulechanges 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) - On login,
-
0
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) -
0
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) -
0Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
-
0
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)


