Copied to clipboard

Flag this post as spam?

This post will be reported to the moderators as potential spam to be looked at


  • Trevor Husseini 15 posts 105 karma points
    Feb 28, 2021 @ 02:57
    Trevor Husseini
    0

    Custom UsersMembershipProvider in Umbraco v8

    Aloha,

    I have a custom UsersMembershipProvider that was built on v7 that I'm trying to adapt for v8. I've read through a lot of the documentation about how to accomplish this and I believe I have it configured correctly, however, I'm not seeing the application break on my override methods.

    To add greater context to the problem, 1 of my clients has a requirement of "a user's password cannot be the same as their last 6 passwords." I too have read M$' white paper on why these arbitrary password rules are pointless given the advent of MFA, but that's a conversation for another time and another place.

    I've successfully extended the BackofficeUserManager and overridden certain methods to add a record to a custom table each time the user updates their password, regardless of if they're performing this action through the back-end or they leverage the password reset functionality. This solves half of my problem as I now have data to compare to. I just can't seem to get PerformPasswordChange to "pop."

    Here are my providers, as listed in my web.config:

        <!-- Membership Provider -->
        <membership defaultProvider="UsersMembershipProvider" userIsOnlineTimeWindow="15">
            <providers>
                <clear/>
                <add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco.Web" minRequiredNonalphanumericCharacters="0"
                    minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false"
                    defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="false"/>
                <add name="UsersMembershipProvider"
                    type="Anthology.Umbraco.v8.Security.MembershipProviders.AnthologyUsersMembershipProvider, Anthology.Umbraco.v8.Security"
                    passwordStrengthRegularExpression="^((?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])|((?=.*[A-Z])(?=.*[0-9])(?=.*(_|[^\\w]))|(?=.*[0-9])(?=.*[a-z])(?=.*(_|[^\\w]))|(?=.*[A-Z])(?=.*[a-z])(?=.*(_|[^\\w])))).{8,256}$"
                    minRequiredNonalphanumericCharacters="0" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="true"
                    requiresQuestionAndAnswer="false" passwordFormat="Hashed"/>
            </providers>
        </membership>
    

    Here's my custom provider:

    using System;
    using System.Web.Security;
    using Anthology.Umbraco.v8.Security.Factories;
    using Umbraco.Web.Security.Providers;
    
    namespace Anthology.Umbraco.v8.Security.MembershipProviders
    {
        public class AnthologyUsersMembershipProvider : UsersMembershipProvider
        {
            // Not getting to here
            protected override bool PerformChangePassword(string username, string oldPassword, string newPassword)
            {
                string str;
                bool flag;
    
                var byUsername = MemberService.GetByUsername(username);
                if (byUsername == null)
                {
                    return false;
                }
                else
                {
                    var service = PasswordServiceFactory.Create();
    
                    string str1 = EncryptOrHashNewPassword(newPassword, out str);
                    byUsername.RawPasswordValue = base.FormatPasswordForStorage(str1, str);
                    byUsername.LastPasswordChangeDate = DateTime.Now;
    
                    // This is where I'm comparing the new password to the history
                    if (service.ValidatePassword(username, byUsername.RawPasswordValue))
                    {
                        MemberService.Save(byUsername, true);
                        flag = true;
                    }
                    else
                    {
                        flag = false;
                    }
                }
    
                return flag;
            }
        }
    }
    

    Does anybody know what I'm doing wrong or another way to approach this problem? Any help would be much appreciated.

    Mahalo!

  • Huw Reddick 380 posts 1062 karma points
    Feb 28, 2021 @ 07:22
    Huw Reddick
    0

    Check the membership settings in the web.config you need to allow members to change their password as by default they are not. I will dig out the exact parameter when I fire my pc up

  • Huw Reddick 380 posts 1062 karma points
    Feb 28, 2021 @ 07:28
    Huw Reddick
    1

    you need to set this attribute

    allowManuallyChangingPassword="true" on the UmbracoMembershipProvider node in your web.config

  • Trevor Husseini 15 posts 105 karma points
    Feb 28, 2021 @ 19:57
    Trevor Husseini
    0

    Thanks for the quick response Huw!

    Unfortunately, that change had no bearing on whether or not my overridden methods were called. I still can't hit my breakpoint in PerformChangePassword, regardless of if the password is changed via reset or through the Backoffice. To make matters worse, it also causes the call to PostChangePassword made on the front-end to return a 400 whenever a user tries to update their password through the Backoffice.

    After initially writing this post, I discovered the UpdatePassword method in the BackOfficeUserManager. I'm hoping I can just override that, then get rid of my custom membership provider all together.

  • Trevor Husseini 15 posts 105 karma points
    Feb 28, 2021 @ 23:25
    Trevor Husseini
    0

    Unfortunately, the alternative approach I mentioned in my previous comment (leveraging the BackOfficeUserManager in lieu of my custom provider) did not pan out. I cannot encrypt and hash the user's new password in the overridden UpdatePassword() method in the same fashion that Umbraco does, so my comparisons of the user's new password to their history consistently fails. Regardless of what method I use, my hashes are always different than what Umbraco ultimately writes to the DB.

    Based on the source code, particularly seeing the protected modifier on the EncryptOrHashNewPassword() method in the MembershipProviderBase class, leads me to believe Umbraco wants you to create a custom membership provider. Protected means I can't access it unless I'm inheriting from UsersMembershipProvider, which my manager is not. More over, I believe the reason the hashes never match is because the salt is constantly changing and the method responsible for generating it is also protected.

    My next approach will more than likely be something that Umbraco HQ won't recommend, and that's to salt, encrypt and hash the passwords using a custom approach, then updating my manager to store those values instead of fetching and storing the hash from the umbracoUser table. That is, unless I can figure out why:

    1. My custom UsersMembershipProvider is not firing
    2. Setting "allowManuallyChangingPassword" to true results in a 400 when changing the password in the Backoffice
  • Trevor Husseini 15 posts 105 karma points
    Mar 01, 2021 @ 04:04
    Trevor Husseini
    0

    The alternative approach in my previous comment panned out from a functional perspective, however, the Backoffice user's experience is not ideal. Umbraco doesn't store the reused password, as expected, but there is no indication of an error (or success for that matter) displayed to the user.

    I already have a custom DelegatingHandler for a different feature that adds custom notifications for when the user publishes a specific type of content. I figured I could leverage that in the same way. Despite being able to detect the request made to /umbraco/backoffice/umbracoapi/currentuser/postchangepassword and parse a custom error that's set in the custom BackOfficeUserManager, I haven't found a way to add the notification. In the case of the pre-existing feature, I'm left with an instance of a ContentItemDisplay, which has a Notifications property that I can add to. In the case of the new feature, I'm left with an instance of System.Web.Http's HttpError.

    Is there a way to fail my overridden UpdatePassword() method such that it returns a model that implements the INotificationModel interface? This looks promising but trying to cast the response content as an instance of that results in the variable being null.

    Below is my overridden UpdatePassword() method in my BackOfficeUserManager:

        protected override async Task<IdentityResult> UpdatePassword(IUserPasswordStore<BackOfficeIdentityUser, int> passwordStore, BackOfficeIdentityUser user, string newPassword)
        {
            var saltBytes = Convert.FromBase64String(_salt);
            var passwordBytes = Convert.FromBase64String(newPassword);
            var hashedBytes = GenerateSaltedHash(passwordBytes, saltBytes);
            var hashedPassword = Convert.ToBase64String(hashedBytes);
    
            var service = PasswordServiceFactory.Create();
            // Ensure password is not the same as the last 6 passwords
            var validPassword = service.CheckPasswordDuplicates(user.UserName, hashedPassword);
    
            if (!validPassword)
            {
                var error = new string[] { "Your password cannot be the same as your last 6 passwords|", user.UserName };
                return IdentityResult.Failed(error);
            }
    
            service.SetPasswordHistory(user.Id, hashedPassword);
    
            return await base.UpdatePassword(passwordStore, user, newPassword);
        }
    
Please Sign in or register to post replies

Write your reply to:

Draft