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:
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.
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
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.
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:
My custom UsersMembershipProvider is not firing
Setting "allowManuallyChangingPassword" to true results in a 400 when changing the password in the Backoffice
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);
}
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:
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.)
Storing the user's last 6 passwords
Ensuring the user's new password is not the same as their last 6 passwords
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.
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:
Here's my custom provider:
Does anybody know what I'm doing wrong or another way to approach this problem? Any help would be much appreciated.
Mahalo!
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
you need to set this attribute
allowManuallyChangingPassword="true" on the UmbracoMembershipProvider node in your web.config
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.
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:
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:
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:
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:
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.
is working on a reply...