BEST
DEALS
OF THE
YEAR!
SAVE UP TO $3,000
LAST DAY 1 DEC
00 Days
00 Hrs
00 Min
00 Sec
Open Closed

Need to whitelist Reset Password path from authorization #10062


User avatar
0
cstobler created

For context, I have a page where my users can add users to their tenant account. I am calling the IdentityUserAppService methods to do this. This is working fine. I am also authorizing the whole app and then whitelisting certain paths and pages in the WebModule to force users to login if they are not:

Configure<RazorPagesOptions>(options =>
{
    options.Conventions.AuthorizeFolder("/");

    options.Conventions.AllowAnonymousToAreaPage("Account", "/Login");
    options.Conventions.AllowAnonymousToAreaPage("Account", "/Register");
    options.Conventions.AllowAnonymousToAreaPage("Account", "/ForgotPassword");
    options.Conventions.AllowAnonymousToAreaPage("Account", "/ResetPassword");
    options.Conventions.AllowAnonymousToAreaPage("Account", "/EmailConfirmation");
    options.Conventions.AllowAnonymousToAreaPage("Account", "/TwoFactor");

    options.Conventions.AllowAnonymousToFolder("/Account");
    options.Conventions.AllowAnonymousToFolder("/Payment");
    options.Conventions.AllowAnonymousToFolder("/Payment/Stripe");
    options.Conventions.AllowAnonymousToFolder("/Public");
    options.Conventions.AllowAnonymousToPage("/Error");
    options.Conventions.AllowAnonymousToPage("/PrivacyPolicy");
    options.Conventions.AllowAnonymousToPage("/Payment/GatewaySelection");
    options.Conventions.AllowAnonymousToPage("/Payment/Stripe/PrePayment");
    options.Conventions.AllowAnonymousToPage("/Payment/Stripe/PostPayment");
});

My issue is that when the user selects ShouldChangePasswordOnNextLogin, it presumably redirects the new user to reset their password. What happens, however, is that the user is kicked back to the login page repeatedly. I am guessing that the user is not logged in at this point for some reason, and the reset password page is not being whitelisted.

Please help me understand what this path would be so I can whitelist it, or let me know if there is a more elegant way to approach this.

Charlie


17 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution
    • What happens with ShouldChangePasswordOnNextLogin:

      • After a successful login, if the user has IdentityUser.ShouldChangePasswordOnNextLogin = true, the Account module redirects the user to the Change Password page (not the email-based ResetPassword page). This page requires the user to be authenticated.
      • Therefore, if your global authorization rules or cookie setup cause a redirect loop, it’s typically because the user is authenticated but your allow-anonymous list or return-url handling interferes, or you didn’t whitelist the correct authenticated page.
    • Pages you should allow and why:

      • Forgot Password and Reset Password are anonymous:
        • /Account/ForgotPassword
        • /Account/ResetPassword
      • Change Password requires authentication (used by ShouldChangePasswordOnNextLogin):
        • /Account/Manage/ChangePassword (MVC UI)
      • Login, Register and other account pages as you already listed.
    • What to change:

      • Keep ForgotPassword and ResetPassword anonymous (as you already did).
      • Do not try to mark ChangePassword as anonymous; the user must be authenticated to use it. Instead, ensure the login succeeds and the auth cookie is established before redirecting there, and that your conventions don’t accidentally block this authenticated page.
      • If you are authorizing the whole “/” folder, do not add AllowAnonymousToFolder("/Account") globally (that would open pages that should be protected). Prefer explicit anonymous pages plus leaving authenticated pages protected.
    • Recommended conventions configuration (minimal and safe):

    Configure<RazorPagesOptions>(options =>
      {
          options.Conventions.AuthorizeFolder("/");
    
          // Anonymous account pages that must be reachable unauthenticated
          options.Conventions.AllowAnonymousToAreaPage("Account", "/Login");
          options.Conventions.AllowAnonymousToAreaPage("Account", "/Register");
          options.Conventions.AllowAnonymousToAreaPage("Account", "/ForgotPassword");
          options.Conventions.AllowAnonymousToAreaPage("Account", "/ResetPassword");
          options.Conventions.AllowAnonymousToAreaPage("Account", "/EmailConfirmation");
          options.Conventions.AllowAnonymousToAreaPage("Account", "/TwoFactor");
    
          // Keep Change Password authenticated (used by ShouldChangePasswordOnNextLogin)
          // Do NOT whitelist it. Path:
          // /Account/Manage/ChangePassword
    
          // Other truly public pages
          options.Conventions.AllowAnonymousToPage("/Error");
          options.Conventions.AllowAnonymousToPage("/PrivacyPolicy");
    
          // Only open payment/public folders if they are truly public
          // options.Conventions.AllowAnonymousToFolder("/Public");
          // options.Conventions.AllowAnonymousToFolder("/Payment");
          // options.Conventions.AllowAnonymousToFolder("/Payment/Stripe");
          // If you must open specific payment pages:
          // options.Conventions.AllowAnonymousToPage("/Payment/GatewaySelection");
          // options.Conventions.AllowAnonymousToPage("/Payment/Stripe/PrePayment");
          // options.Conventions.AllowAnonymousToPage("/Payment/Stripe/PostPayment");
      });
    
    • If you still get redirected back to login repeatedly:

      • Confirm the login actually succeeds (no external cookie issues, no tenant mismatch).
      • Ensure you don’t have AllowAnonymousToFolder("/") or AllowAnonymousToFolder("/Account") combined with AuthorizeFolder("/") creating conflicting behaviors.
      • Make sure the application cookie is issued and the auth scheme matches the one used by ABP (default cookie + OIDC if used).
      • If you have custom middleware that redirects unauthenticated users, ensure it doesn’t intercept the ChangePassword redirect.
    • Summary of the path to whitelist vs. not:

      • Whitelist (anonymous): /Account/ForgotPassword, /Account/ResetPassword, /Account/Login, /Account/Register, /Account/EmailConfirmation, /Account/TwoFactor.
      • Do not whitelist (must stay authorized): /Account/Manage/ChangePassword (this is where ShouldChangePasswordOnNextLogin sends the user).
    1. Sources
    • Account Module (Forgot/Reset password are anonymous flows; change password is for authenticated users)
      • https://abp.io/docs/latest/modules/account
    • Customizing MVC/Razor Pages and authorization conventions
      • https://abp.io/docs/latest/framework/ui/aspnet-core/mvc/razor-pages

    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.

  • User Avatar
    0
    cstobler created

    That doesn't really recommend anything aside from removing the whitelisting on some of my other pages, which I need for other purposes in my app (my own Stripe implementation).

    Any help would be appreciated.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can reconsider requiring authentication for all pages.

    options.Conventions.AuthorizeFolder("/");

    Perhaps you can find an alternative solution.

    What is your real use case?

    Thanks.

  • User Avatar
    0
    cstobler created

    Well, I set it up this way because I didn't want to utilize the "host" section of the app (as in, I wanted all users to be tenants), and I wanted to make sure that whenever someone was not logged in, they were redirected to the login page. This seemed like a good way to do it at the time since it forced login before users could access the app and it forced them to be tenants.

    Is there a better way to go about this?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can disable the self-registration on the Host side.

    Thanks.

  • User Avatar
    0
    cstobler created

    Can you explain this in more detail please? If self-registration is disabled on the host side, will it still allow self-registration for tenants? I need tenants to be able to sign up themselves, but I don't want anyone accessing the host side.

    If this will work for my purposes, then is there some code I can implement that will set that rather than configuring it in the host settings in the app? Mainly so it is more streamlined when I need to deploy elsewhere.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    You can enable it in Tenant and disable it on the host side.

    Thanks.

  • User Avatar
    0
    cstobler created

    I have modified a fair amount at this point, so I am not able to get the "Administration" section to show up in Host, only when logged in. I have var administration = context.Menu.GetAdministration(); in my menu contributor, and I have commented out the code i provided above that authorizes the whole app. It allows me to access Host but I still cannot access Administration.

    Here is my MenuContributor:

    private static Task ConfigureMainMenuAsync(MenuConfigurationContext context)
    {
        var l = context.GetLocalizer<ArmadaResource>();
    
        var currentUser = context.ServiceProvider.GetRequiredService<ICurrentUser>();
        var currentTenant = context.ServiceProvider.GetRequiredService<ICurrentTenant>();
        bool isHostAdmin = currentUser.IsAuthenticated && currentTenant.Id == null;
    
        //Home
        context.Menu.AddItem(
            new ApplicationMenuItem(
                ArmadaIOMenus.Home,
                l["Menu:Home"],
                "~/",
                icon: "fa fa-home",
                order: 1
            )
        );
    
        //HostDashboard
        context.Menu.AddItem(
            new ApplicationMenuItem(
                ArmadaIOMenus.HostDashboard,
                l["Menu:Dashboard"],
                "~/HostDashboard",
                icon: "fa fa-line-chart",
                order: 2
            ).RequirePermissions(ArmadaIOPermissions.Dashboard.Host)
        );
    
        //TenantDashboard
        //context.Menu.AddItem(
        //    new ApplicationMenuItem(
        //        ArmadaIOMenus.TenantDashboard,
        //        l["Menu:Dashboard"],
        //        "~/Dashboard",
        //        icon: "fa fa-line-chart",
        //        order: 2
        //    ).RequirePermissions(ArmadaIOPermissions.Dashboard.Tenant)
        //);
    
        context.Menu.AddItem(
            new ApplicationMenuItem(
                ArmadaIOMenus.Forms,
                l[ArmadaIOMenus.Forms],
                url: "/Forms",
                icon: "fas fa-file",
                order: 1
            )
        );
    
        context.Menu.AddItem(
            new ApplicationMenuItem(
                ArmadaIOMenus.Calendar,
                l[ArmadaIOMenus.Calendar],
                url: "/Calendar",
                icon: "fas fa-calendar-alt",
                order: 2
            )
        );
    
        context.Menu.AddItem(
            new ApplicationMenuItem(
                ArmadaIOMenus.Settings,
                l[ArmadaIOMenus.Settings],
                icon: "fas fa-gear",
                order: 2
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.GeneralSettings,
                    l[ArmadaIOMenus.GeneralSettings],
                    url: "/Settings"
                )
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.CompanyLocations,
                    l[ArmadaIOMenus.CompanyLocations],
                    url: "/Settings/CompanyLocations"
                )
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.Drivers,
                    l[ArmadaIOMenus.Drivers],
                    url: "/Settings/Drivers"
                )
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.LimitedItems,
                    l[ArmadaIOMenus.LimitedItems],
                    url: "/LimitedResources"
                )
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.Payments,
                    l[ArmadaIOMenus.Payments],
                    url: "/Settings/Payments/StripeAccount"
                )
            ).AddItem(
                new ApplicationMenuItem(
                    ArmadaIOMenus.Users,
                    l[ArmadaIOMenus.Users],
                    url: "/Settings/Users"
                )
            )
        );
    
        //Saas
        context.Menu.SetSubItemOrder(SaasHostMenuNames.GroupName, 3);
    
    
    
        //Administration
        var administration = context.Menu.GetAdministration();
        //if (!isHostAdmin)
        //{
        //    context.Menu.Items.Remove(administration);
        //}
    
        administration.Order = 6;
    
        //Administration->Identity
        administration.SetSubItemOrder(IdentityMenuNames.GroupName, 2);
    
        //Administration->OpenIddict
        administration.SetSubItemOrder(OpenIddictProMenus.GroupName, 3);
    
        //Administration->Language Management
        administration.SetSubItemOrder(LanguageManagementMenuNames.GroupName, 4);
    
        //Administration->Text Template Management
        administration.SetSubItemOrder(TextTemplateManagementMainMenuNames.GroupName, 5);
    
        //Administration->Audit Logs
        administration.SetSubItemOrder(AbpAuditLoggingMainMenuNames.GroupName, 6);
    
        //Administration->Settings
        administration.SetSubItemOrder(SettingManagementMenuNames.GroupName, 7);
        
        return Task.CompletedTask;
    }
    

    However, why do you think I should move away from my existing solution? I would at least like to be able to get the path to the resetpassword page so I can whitelist it as an option in case I need it.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    it presumably redirects the new user to reset their password. What happens, however, is that the user is kicked back to the login page repeatedly.

    If you want to solve this problem, you can try to add:

    options.Conventions.AllowAnonymousToAreaPage("Account", "/ChangePassword");

    Thanks.

  • User Avatar
    0
    cstobler created

    Thank you for that. I attempted it, but it didn't work, so I tried removing all of my authorization code (so I can access host pages and am not locked out of anything by default) and tried to see if I can at least get to the ChangePassword page, but it still isn't working. At this point, I don't think there is anything on my end that is preventing this, though I could be wrong. The main thing that is different is that I am creating users through my own appservice:

    public async Task UpsertUserAsync(Guid userId, string email, string? password, bool forceChangePassword)
    {
        if (userId != Guid.Empty)
        {
            IdentityUserDto existingUser = await _identityUserAppService.GetAsync(userId);
            if (existingUser == null)
            {
                throw new UserFriendlyException("User not found.");
            }
    
            string username = existingUser.UserName == "admin" ? "admin" : email;
    
            IdentityUserUpdateDto updateDto = new IdentityUserUpdateDto
            {
                UserName = username,
                Email = email,
                ShouldChangePasswordOnNextLogin = forceChangePassword,
                IsActive = true,
                EmailConfirmed = true,
                PhoneNumberConfirmed = true,
                RoleNames = ["admin"]
            };
            await _identityUserAppService.UpdateAsync(userId, updateDto);
            return;
        }
    
        // Make sure email is unique
        bool isEmailUnique = await IsEmailUnique(email);
        if (!isEmailUnique)
        {
            throw new UserFriendlyException("Email address is already registered. Please use another email address.");
        }
    
        await _identityUserAppService.CreateAsync(new IdentityUserCreateDto
        {
            UserName = email,
            Email = email,
            Password = password,
            ShouldChangePasswordOnNextLogin = forceChangePassword,
            IsActive = true,
            EmailConfirmed = true,
            PhoneNumberConfirmed = true,
            RoleNames = ["admin"]
        });
    }
    

    Could there be something wrong with how I am creating the users here which might impact their ability to reach that page? There are some things I will likely need to resolve here like verifying emails, etc, but as a proof of concept, I think this should work, and it does when ShouldChangePasswordOnNextLogin is set to false, but not when it is set to true.

    Let me know if I can provide any more info.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Can you use a new template project to reproduce your current problem and share it?

    liming.ma@volosoft.com

    Thanks

  • User Avatar
    0
    cstobler created

    It will take me a little while to create a new project and add the necessary code to recreate this. I will try to get to this tomorrow.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    Thanks. Please share the test project and steps to reproduce the problem.

    👍

  • User Avatar
    0
    cstobler created

    Ok, I made a lot of progress towards figuring out why this is occurring, but I'm not sure how to fix it with my current implementation (will explain below)

    First, I created the fresh template solution to attempt to recreate the problem, and of course, it worked in the new project. However, I didn't have anything that was intentionally blocking the ChangePassword page, so I started testing a bunch of different configurations to see if I could get it to work.

    I found out that in my existing project, using the default login model rather than my overridden login model, it worked as expected. I further tracked this down to the tenant switcher section. In my custom login page, I removed the tenant switcher, instead opting to infer the tenant from the email entered and use that to switch to the corresponding tenant in the code. See my custom login model OnPostAsync:

    public override async Task<IActionResult> OnPostAsync(string action)
    {
        try
        {
            if (LoginInput.UserNameOrEmailAddress == "admin")
            {
                return await base.OnPostAsync(action);
            }
    
            Guid tenantId = await _multiTenancyAppService.GetTenantIdByEmailAsync(LoginInput.UserNameOrEmailAddress);
            if (tenantId == Guid.Empty)
            {
                ModelState.AddModelError(string.Empty, "Account not found. Please register if you do not have an account.");
                return Page();
            }
    
            using (CurrentTenant.Change(tenantId))
            {
                return await base.OnPostAsync(action);
            }
        }
        catch (UserFriendlyException ex)
        {
            ViewData["ErrorMessage"] = ex.Message;
            await base.OnGetAsync();
            return Page();
        }
    }
    

    That specific section:

    using (CurrentTenant.Change(tenantId))
    {
        return await base.OnPostAsync(action);
    }
    

    works normally to log the user in to their tenant without requiring them to switch tenants in the login page (I want my users to be as multitenancy-unaware as possible)

    However, that doesn't work with the change password page. If I have the tenant null and log into a normal account, it will properly infer the tenant by the email, change tenant in code, and login. If I have the tenant null and attempt to log into an account that has ShouldChangePasswordOnNextLogin set to true, it kicks me back to login page. If I manually switch the tenant using the tenant switcher in the UI, then it works.

    So here is the question: Is this a bug? Why does using (CurrentTenant.Change(tenantId)) work for logged in, but not for being redirected to the /ChangePassword page?

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    using (CurrentTenant.Change(tenantId)) only works in the using scope. In the ChangePassword page, CurrentTenant will be determined via default rules: https://abp.io/docs/latest/framework/architecture/multi-tenancy#determining-the-current-tenant

    Thanks.

  • User Avatar
    0
    cstobler created

    Ok, that makes sense. I am not using any tenant resolvers like subdomain tenant resolver, so I decided to just set the tenant cookie when logging in. This seems to have resolved my issue.

    Thanks for your help.

  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Great

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.1.0-preview. Updated on November 20, 2025, 09:12
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.