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 20 posts 151 karma points
    Feb 28, 2021 @ 02:57
    Trevor Husseini
    1

    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 1737 posts 6098 karma points MVP c-trib
    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 1737 posts 6098 karma points MVP c-trib
    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 20 posts 151 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 20 posts 151 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 20 posts 151 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);
        }
    
  • Trevor Husseini 20 posts 151 karma points
    Jan 07, 2022 @ 00:28
    Trevor Husseini
    100

    Circling back on this just in case anyone else experiences a similar frustration when trying to port a custom password complexity solution developed on v7 over to v8. Ultimately, the problems we faced which required custom development were:

    1. Capturing the date/time the last time a user logged in (Note: this snippet is redundant as you should be able to rely on the lastLoginDate column in the umbracoUser instead of capturing this data elsewhere.)
    2. Storing the user's last 6 passwords
    3. Ensuring the user's new password is not the same as their last 6 passwords
    4. Ensuring the user is not attempting to change their password within 24 hours of last changing their password

    All of the other complexity requirements were handled via the default properties of the default UsersMembershipProvider.

    The final solution ended up being to replace the custom Umbraco membership provider with a custom BackofficeUserManager. Then, create a startup class that is derived from the UmbracoDefaultOwinStartup that configures your services. Finally, update the owin:appStartup appsetting in the web app's web.config after you've added the reference to the custom DLL. See below for the full manager:

    using Client.Umbraco.v8.Security.Factories;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.Owin;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Umbraco.Core.Configuration.UmbracoSettings;
    using Umbraco.Core.Models.Identity;
    using Umbraco.Core.Security;
    using Umbraco.Web.Security;
    
    namespace Client.Umbraco.v8.Security.Managers
    {
        public class ClientBackOfficeUserManager : BackOfficeUserManager
        {
            public ClientBackOfficeUserManager(IUserStore<BackOfficeIdentityUser, int> store, IdentityFactoryOptions<BackOfficeUserManager> options, 
                MembershipProviderBase membershipProvider, IContentSection contentSectionConfig) 
                : base(store, options, membershipProvider, contentSectionConfig) { }
    
            #region Protected Override Methods
    
            // Invoked each time a user logs in
            protected override void OnLoginSuccess(IdentityAuditEventArgs e)
            {
                int userId = e.AffectedUser;
                var service = PasswordServiceFactory.Create();
                service.SetLastLogin(userId);
    
                base.OnLoginSuccess(e);
            }
    
            // Invoked each time a user changes their password, including on reset
            protected override async Task<IdentityResult> UpdatePassword(IUserPasswordStore<BackOfficeIdentityUser, int> passwordStore, BackOfficeIdentityUser user, string newPassword)
            {
                var service = PasswordServiceFactory.Create();
                // Ensure password is not the same as the last 6 passwords
                var validPassword = service.CheckPasswordDuplicates(user.UserName, newPassword);
                // Ensure password meets age requirements
                var ofAge = service.CheckPasswordMinimumAge(user.UserName);
    
                List<string> result = new List<string>();
    
                // Obfuscate error message to prevent exposing business rules to potentially malicious parties
                if (!validPassword || !ofAge)
                {
                    result.Add("Password reset failed. Please try again later or contact your System Administrator if you continue to have issues.");
                }
    
                if (result.Count != 0)
                {
                    // Note: error messages displayed on password reset, but not on set
                    return IdentityResult.Failed(string.Join(" ", (IEnumerable<string>)result));
                }
    
                // Let Umbraco validate and store the new password
                var updatePassword =  await base.UpdatePassword(passwordStore, user, newPassword);
    
                if (updatePassword.Succeeded)
                {
                    // Capture updated password on success
                    service.SetPasswordHistory(user.Id, newPassword);
                }
    
                return updatePassword;
            }
    
            #endregion
        }
    }
    

    We also leveraged this fantastic project from Sebastiaan Janssen to supplement the removal of the scheduled tasks from v7. Not only were we able to create tasks to disable users who hadn't logged in in over 150 days, we can send users who haven't changed their passwords in 90 days a reminder about their password expiring as well as programmatically resetting their passwords if they fail to change it before that time runs out. It also comes with a sweet UI in the dashboard where you can see the task results, run them manually, etc.! I highly recommend that project for all scheduling needs and kudos to Sebastiaan for working on Umbraco, even when he's not at HQ.

Please Sign in or register to post replies

Write your reply to:

Draft