Gets password roll forward working

This commit is contained in:
Shannon
2020-10-07 16:56:48 +11:00
parent e5c272b5d2
commit eaa295095d
14 changed files with 157 additions and 437 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text;
using Umbraco.Core.Configuration;
@@ -11,111 +12,28 @@ namespace Umbraco.Core.Security
/// </summary>
public class LegacyPasswordSecurity
{
private readonly IPasswordConfiguration _passwordConfiguration;
private readonly PasswordGenerator _generator;
/// <summary>
/// Constructor
/// </summary>
/// <param name="passwordConfiguration"></param>
public LegacyPasswordSecurity(IPasswordConfiguration passwordConfiguration)
{
_passwordConfiguration = passwordConfiguration;
_generator = new PasswordGenerator(passwordConfiguration);
}
public string GeneratePassword() => _generator.GeneratePassword();
/// <summary>
/// Returns a hashed password value used to store in a data store
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string HashPasswordForStorage(string password)
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
public string HashPasswordForStorage(string algorithmType, string password)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("password cannot be empty", nameof(password));
string salt;
var hashed = HashNewPassword(_passwordConfiguration.HashAlgorithmType, password, out salt);
return FormatPasswordForStorage(hashed, salt);
var hashed = HashNewPassword(algorithmType, password, out salt);
return FormatPasswordForStorage(algorithmType, hashed, salt);
}
/// <summary>
/// If the password format is a hashed keyed algorithm then we will pre-pend the salt used to hash the password
/// to the hashed password itself.
/// </summary>
/// <param name="hashedPassword"></param>
/// <param name="salt"></param>
/// <returns></returns>
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string FormatPasswordForStorage(string hashedPassword, string salt)
{
return salt + hashedPassword;
}
/// <summary>
/// Hashes a password with a given salt
/// </summary>
/// <param name="algorithmType">The hashing algorithm for the password.</param>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
public string HashPassword(string algorithmType, string pass, string salt)
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
{
return HashLegacySHA1Password(pass);
return hashedPassword;
}
//This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
var hashAlgorithm = GetHashAlgorithm(algorithmType);
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
var keyedHashAlgorithm = algorithm;
if (keyedHashAlgorithm.Key.Length == saltBytes.Length)
{
//if the salt bytes is the required key length for the algorithm, use it as-is
keyedHashAlgorithm.Key = saltBytes;
}
else if (keyedHashAlgorithm.Key.Length < saltBytes.Length)
{
//if the salt bytes is too long for the required key length for the algorithm, reduce it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length);
keyedHashAlgorithm.Key = numArray2;
}
else
{
//if the salt bytes is too short for the required key length for the algorithm, extend it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
var dstOffset = 0;
while (dstOffset < numArray2.Length)
{
var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset);
Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count);
dstOffset += count;
}
keyedHashAlgorithm.Key = numArray2;
}
inArray = keyedHashAlgorithm.ComputeHash(bytes);
}
else
{
var buffer = new byte[saltBytes.Length + bytes.Length];
Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length);
Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length);
inArray = hashAlgorithm.ComputeHash(buffer);
}
return Convert.ToBase64String(inArray);
return salt + hashedPassword;
}
/// <summary>
@@ -182,6 +100,69 @@ namespace Umbraco.Core.Security
return Convert.ToBase64String(numArray);
}
/// <summary>
/// Hashes a password with a given salt
/// </summary>
/// <param name="algorithmType">The hashing algorithm for the password.</param>
/// <param name="pass"></param>
/// <param name="salt"></param>
/// <returns></returns>
private string HashPassword(string algorithmType, string pass, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
{
return HashLegacySHA1Password(pass);
}
//This is the correct way to implement this (as per the sql membership provider)
var bytes = Encoding.Unicode.GetBytes(pass);
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
var hashAlgorithm = GetHashAlgorithm(algorithmType);
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
var keyedHashAlgorithm = algorithm;
if (keyedHashAlgorithm.Key.Length == saltBytes.Length)
{
//if the salt bytes is the required key length for the algorithm, use it as-is
keyedHashAlgorithm.Key = saltBytes;
}
else if (keyedHashAlgorithm.Key.Length < saltBytes.Length)
{
//if the salt bytes is too long for the required key length for the algorithm, reduce it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
Buffer.BlockCopy(saltBytes, 0, numArray2, 0, numArray2.Length);
keyedHashAlgorithm.Key = numArray2;
}
else
{
//if the salt bytes is too short for the required key length for the algorithm, extend it
var numArray2 = new byte[keyedHashAlgorithm.Key.Length];
var dstOffset = 0;
while (dstOffset < numArray2.Length)
{
var count = Math.Min(saltBytes.Length, numArray2.Length - dstOffset);
Buffer.BlockCopy(saltBytes, 0, numArray2, dstOffset, count);
dstOffset += count;
}
keyedHashAlgorithm.Key = numArray2;
}
inArray = keyedHashAlgorithm.ComputeHash(bytes);
}
else
{
var buffer = new byte[saltBytes.Length + bytes.Length];
Buffer.BlockCopy(saltBytes, 0, buffer, 0, saltBytes.Length);
Buffer.BlockCopy(bytes, 0, buffer, saltBytes.Length, bytes.Length);
inArray = hashAlgorithm.ComputeHash(buffer);
}
return Convert.ToBase64String(inArray);
}
/// <summary>
/// Return the hash algorithm to use based on the <see cref="IPasswordConfiguration"/>
/// </summary>

View File

@@ -246,6 +246,7 @@ namespace Umbraco.Core.BackOffice
if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
user.PasswordHash = passwordHash;
user.PasswordConfig = null; // Clear this so that it's reset at the repository level
return Task.CompletedTask;
}

View File

@@ -1,6 +1,5 @@
using Moq;
using NUnit.Framework;
using System.Security.Cryptography;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
@@ -8,19 +7,19 @@ using Umbraco.Core.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
{
[TestFixture]
public class PasswordSecurityTests
public class LegacyPasswordSecurityTests
{
[Test]
public void Check_Password_Hashed_Non_KeyedHashAlgorithm()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "SHA256");
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
var passwordSecurity = new LegacyPasswordSecurity();
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(passwordConfiguration.HashAlgorithmType, pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(passwordConfiguration.HashAlgorithmType, hashed, salt);
var result = passwordSecurity.VerifyPassword(passwordConfiguration.HashAlgorithmType, "ThisIsAHashedPassword", storedPassword);
@@ -31,12 +30,28 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
public void Check_Password_Hashed_KeyedHashAlgorithm()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
var passwordSecurity = new LegacyPasswordSecurity();
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(passwordConfiguration.HashAlgorithmType, pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(passwordConfiguration.HashAlgorithmType, hashed, salt);
var result = passwordSecurity.VerifyPassword(passwordConfiguration.HashAlgorithmType, "ThisIsAHashedPassword", storedPassword);
Assert.IsTrue(result);
}
[Test]
public void Check_Password_Legacy_v4_SHA1()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity();
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(passwordConfiguration.HashAlgorithmType, pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(passwordConfiguration.HashAlgorithmType, hashed, salt);
var result = passwordSecurity.VerifyPassword(passwordConfiguration.HashAlgorithmType, "ThisIsAHashedPassword", storedPassword);
@@ -46,12 +61,13 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
[Test]
public void Format_Pass_For_Storage_Hashed()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity();
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = "ThisIsAHashedPassword";
var result = passwordSecurity.FormatPasswordForStorage(stored, salt);
var result = passwordSecurity.FormatPasswordForStorage(passwordConfiguration.HashAlgorithmType, stored, salt);
Assert.AreEqual(salt + "ThisIsAHashedPassword", result);
}
@@ -60,7 +76,7 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
public void Get_Stored_Password_Hashed()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity(passwordConfiguration);
var passwordSecurity = new LegacyPasswordSecurity();
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = salt + "ThisIsAHashedPassword";

View File

@@ -1,165 +0,0 @@
using System;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Security.Cryptography;
using System.Web.Security;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Security;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using Umbraco.Web.Composing;
using Umbraco.Web.Security;
namespace Umbraco.Tests.Membership
{
[TestFixture]
[UmbracoTest(WithApplication = true)]
public class MembershipProviderBaseTests : UmbracoTestBase
{
[Test]
public void ChangePasswordQuestionAndAnswer_Without_RequiresQuestionAndAnswer()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.RequiresQuestionAndAnswer).Returns(false);
var provider = providerMock.Object;
Assert.Throws<NotSupportedException>(() => provider.ChangePasswordQuestionAndAnswer("test", "test", "test", "test"));
}
[Test]
public void CreateUser_Not_Whitespace()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) {CallBase = true};
var provider = providerMock.Object;
MembershipCreateStatus status;
var result = provider.CreateUser("", "", "test@test.com", "", "", true, "", out status);
Assert.IsNull(result);
Assert.AreEqual(MembershipCreateStatus.InvalidUserName, status);
}
[Test]
public void CreateUser_Invalid_Question()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.RequiresQuestionAndAnswer).Returns(true);
var provider = providerMock.Object;
MembershipCreateStatus status;
var result = provider.CreateUser("test", "test", "test@test.com", "", "", true, "", out status);
Assert.IsNull(result);
Assert.AreEqual(MembershipCreateStatus.InvalidQuestion, status);
}
[Test]
public void CreateUser_Invalid_Answer()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.RequiresQuestionAndAnswer).Returns(true);
var provider = providerMock.Object;
MembershipCreateStatus status;
var result = provider.CreateUser("test", "test", "test@test.com", "test", "", true, "", out status);
Assert.IsNull(result);
Assert.AreEqual(MembershipCreateStatus.InvalidAnswer, status);
}
[Test]
public void GetPassword_Without_EnablePasswordRetrieval()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.EnablePasswordRetrieval).Returns(false);
var provider = providerMock.Object;
Assert.Throws<ProviderException>(() => provider.GetPassword("test", "test"));
}
[Test]
public void GetPassword_With_Hashed()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.EnablePasswordRetrieval).Returns(true);
providerMock.Setup(@base => @base.PasswordFormat).Returns(MembershipPasswordFormat.Hashed);
var provider = providerMock.Object;
Assert.Throws<ProviderException>(() => provider.GetPassword("test", "test"));
}
// FIXME: in v7 this test relies on ApplicationContext.Current being null, which makes little
// sense, not going to port the weird code in MembershipProviderBase.ResetPassword, so
// what shall we do?
[Test]
[Ignore("makes no sense?")]
public void ResetPassword_Without_EnablePasswordReset()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
providerMock.Setup(@base => @base.EnablePasswordReset).Returns(false);
var provider = providerMock.Object;
Assert.Throws<NotSupportedException>(() => provider.ResetPassword("test", "test"));
}
[Test]
public void Sets_Defaults()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
var provider = providerMock.Object;
provider.Initialize("test", new NameValueCollection());
Assert.AreEqual("test", provider.Name);
Assert.AreEqual(MembershipProviderBase.GetDefaultAppName(TestHelper.GetHostingEnvironment()), provider.ApplicationName);
Assert.AreEqual(false, provider.EnablePasswordRetrieval);
Assert.AreEqual(true, provider.EnablePasswordReset);
Assert.AreEqual(false, provider.RequiresQuestionAndAnswer);
Assert.AreEqual(true, provider.RequiresUniqueEmail);
Assert.AreEqual(5, provider.MaxInvalidPasswordAttempts);
Assert.AreEqual(10, provider.PasswordAttemptWindow);
Assert.AreEqual(provider.DefaultMinPasswordLength, provider.MinRequiredPasswordLength);
Assert.AreEqual(provider.DefaultMinNonAlphanumericChars, provider.MinRequiredNonAlphanumericCharacters);
Assert.AreEqual(null, provider.PasswordStrengthRegularExpression);
Assert.AreEqual(provider.DefaultUseLegacyEncoding, provider.UseLegacyEncoding);
Assert.AreEqual(MembershipPasswordFormat.Hashed, provider.PasswordFormat);
}
[Test]
public void Throws_Exception_With_Hashed_Password_And_Password_Retrieval()
{
var providerMock = new Mock<MembershipProviderBase>(TestHelper.GetHostingEnvironment()) { CallBase = true };
var provider = providerMock.Object;
Assert.Throws<ProviderException>(() => provider.Initialize("test", new NameValueCollection()
{
{"enablePasswordRetrieval", "true"},
{"passwordFormat", "Hashed"}
}));
}
[TestCase("hello", 0, "", 5, true)]
[TestCase("hello", 0, "", 4, true)]
[TestCase("hello", 0, "", 6, false)]
[TestCase("hello", 1, "", 5, false)]
[TestCase("hello!", 1, "", 0, true)]
[TestCase("hello!", 2, "", 0, false)]
[TestCase("hello!!", 2, "", 0, true)]
//8 characters or more in length, at least 1 lowercase letter,at least 1 character that is not a lower letter.
[TestCase("hello", 0, "(?=.{8,})[a-z]+[^a-z]+|[^a-z]+[a-z]+", 0, false)]
[TestCase("helloooo", 0, "(?=.{8,})[a-z]+[^a-z]+|[^a-z]+[a-z]+", 0, false)]
[TestCase("helloooO", 0, "(?=.{8,})[a-z]+[^a-z]+|[^a-z]+[a-z]+", 0, true)]
[TestCase("HELLOOOO", 0, "(?=.{8,})[a-z]+[^a-z]+|[^a-z]+[a-z]+", 0, false)]
[TestCase("HELLOOOo", 0, "(?=.{8,})[a-z]+[^a-z]+|[^a-z]+[a-z]+", 0, true)]
public void Valid_Password(string password, int minRequiredNonAlphanumericChars, string strengthRegex, int minLength, bool pass)
{
var result = MembershipProviderBase.IsPasswordValid(password, minRequiredNonAlphanumericChars, strengthRegex, minLength);
Assert.AreEqual(pass, result.Success);
}
}
}

View File

@@ -1,115 +0,0 @@
using System.Collections.Specialized;
using System.Threading;
using System.Web.Security;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
using Umbraco.Web.Security.Providers;
namespace Umbraco.Tests.Membership
{
[TestFixture]
[Apartment(ApartmentState.STA)]
[UmbracoTest(Database = UmbracoTestOptions.Database.None, WithApplication = true)]
public class UmbracoServiceMembershipProviderTests : UmbracoTestBase
{
[Test]
public void Sets_Default_Member_Type_From_Service_On_Init()
{
var memberTypeServiceMock = new Mock<IMemberTypeService>();
memberTypeServiceMock.Setup(x => x.GetDefault()).Returns("Blah");
var provider = new MembersMembershipProvider(Mock.Of<IMembershipMemberService>(), memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection());
Assert.AreEqual("Blah", provider.DefaultMemberTypeAlias);
}
[Test]
public void Sets_Default_Member_Type_From_Config_On_Init()
{
var memberTypeServiceMock = new Mock<IMemberTypeService>();
memberTypeServiceMock.Setup(x => x.GetDefault()).Returns("Blah");
var provider = new MembersMembershipProvider(Mock.Of<IMembershipMemberService>(), memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection { { "defaultMemberTypeAlias", "Hello" } });
Assert.AreEqual("Hello", provider.DefaultMemberTypeAlias);
}
[Test]
public void Create_User_Already_Exists()
{
var memberTypeServiceMock = new Mock<IMemberTypeService>();
memberTypeServiceMock.Setup(x => x.GetDefault()).Returns("Member");
var membershipServiceMock = new Mock<IMembershipMemberService>();
membershipServiceMock.Setup(service => service.Exists("test")).Returns(true);
var provider = new MembersMembershipProvider(membershipServiceMock.Object, memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection());
MembershipCreateStatus status;
var user = provider.CreateUser("test", "test", "testtest$1", "test@test.com", "test", "test", true, "test", out status);
Assert.IsNull(user);
}
[Test]
public void Create_User_Requires_Unique_Email()
{
var memberTypeServiceMock = new Mock<IMemberTypeService>();
memberTypeServiceMock.Setup(x => x.GetDefault()).Returns("Member");
var membershipServiceMock = new Mock<IMembershipMemberService>();
membershipServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => new Member("test", MockedContentTypes.CreateSimpleMemberType()));
var provider = new MembersMembershipProvider(membershipServiceMock.Object, memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection { { "requiresUniqueEmail", "true" } });
MembershipCreateStatus status;
var user = provider.CreateUser("test", "test", "testtest$1", "test@test.com", "test", "test", true, "test", out status);
Assert.IsNull(user);
}
[Test]
public void Password_Hashed_With_Salt()
{
IMember createdMember = null;
var memberType = MockedContentTypes.CreateSimpleMemberType();
foreach (var p in ConventionsHelper.GetStandardPropertyTypeStubs(TestHelper.ShortStringHelper))
{
memberType.AddPropertyType(p.Value);
}
var memberTypeServiceMock = new Mock<IMemberTypeService>();
memberTypeServiceMock.Setup(x => x.GetDefault()).Returns("Member");
var membershipServiceMock = new Mock<IMembershipMemberService>();
membershipServiceMock.Setup(service => service.Exists("test")).Returns(false);
membershipServiceMock.Setup(service => service.GetByEmail("test@test.com")).Returns(() => null);
membershipServiceMock.Setup(
service => service.CreateWithIdentity(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<bool>()))
.Callback((string u, string e, string p, string m, bool isApproved) =>
{
createdMember = new Member("test", e, u, p, memberType, isApproved);
})
.Returns(() => createdMember);
var provider = new MembersMembershipProvider(membershipServiceMock.Object, memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection { { "passwordFormat", "Hashed" }, { "hashAlgorithmType", Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName } });
MembershipCreateStatus status;
provider.CreateUser("test", "test", "testtest$1", "test@test.com", "test", "test", true, "test", out status);
Assert.AreNotEqual("test", createdMember.RawPasswordValue);
string salt;
var storedPassword = provider.PasswordSecurity.ParseStoredHashPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, createdMember.RawPasswordValue, out salt);
var hashedPassword = provider.PasswordSecurity.HashPassword(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName, "testtest$1", salt);
Assert.AreEqual(hashedPassword, storedPassword);
}
}
}

View File

@@ -234,8 +234,6 @@
<Compile Include="Web\Mvc\ValidateUmbracoFormRouteStringAttributeTests.cs" />
<Compile Include="Web\PublishedContentQueryTests.cs" />
<Compile Include="Web\UmbracoHelperTests.cs" />
<Compile Include="Membership\MembershipProviderBaseTests.cs" />
<Compile Include="Membership\UmbracoServiceMembershipProviderTests.cs" />
<Compile Include="Migrations\Stubs\FiveZeroMigration.cs" />
<Compile Include="Migrations\Stubs\FourElevenMigration.cs" />
<Compile Include="Migrations\Stubs\SixZeroMigration2.cs" />

View File

@@ -305,18 +305,22 @@ namespace Umbraco.Web.BackOffice.Controllers
private IMember CreateMemberData(MemberSave contentItem)
{
var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
if (memberType == null)
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
{
CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
Comments = contentItem.Comments,
IsApproved = contentItem.IsApproved
};
throw new NotImplementedException("Members have not been migrated to netcore");
return member;
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
//var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
//if (memberType == null)
// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
//var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
//{
// CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
// Comments = contentItem.Comments,
// IsApproved = contentItem.IsApproved
//};
//return member;
}
/// <summary>
@@ -372,8 +376,10 @@ namespace Umbraco.Web.BackOffice.Controllers
if (contentItem.Password == null)
return;
throw new NotImplementedException("Members have not been migrated to netcore");
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
// set the password
contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword);
//contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword);
}
private static void UpdateName(MemberSave memberSave)

View File

@@ -86,7 +86,7 @@ namespace Umbraco.Extensions
services.TryAddScoped<IPasswordValidator<BackOfficeIdentityUser>, PasswordValidator<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordHasher<BackOfficeIdentityUser>>(
services => new BackOfficePasswordHasher(
new LegacyPasswordSecurity(services.GetRequiredService<IOptions<UserPasswordConfigurationSettings>>().Value),
new LegacyPasswordSecurity(),
services.GetRequiredService<IJsonSerializer>()));
services.TryAddScoped<IUserConfirmation<BackOfficeIdentityUser>, DefaultUserConfirmation<BackOfficeIdentityUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>, UserClaimsPrincipalFactory<BackOfficeIdentityUser>>();

View File

@@ -11,10 +11,14 @@ namespace Umbraco.Web.BackOffice.Security
/// <summary>
/// A password hasher for back office users
/// </summary>
/// <remarks>
/// This allows us to verify passwords in old formats and roll forward to the latest format
/// </remarks>
public class BackOfficePasswordHasher : PasswordHasher<BackOfficeIdentityUser>
{
private readonly LegacyPasswordSecurity _legacyPasswordSecurity;
private readonly IJsonSerializer _jsonSerializer;
private readonly PasswordHasher<BackOfficeIdentityUser> _aspnetV2PasswordHasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
public BackOfficePasswordHasher(LegacyPasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
{
@@ -24,37 +28,23 @@ namespace Umbraco.Web.BackOffice.Security
public override string HashPassword(BackOfficeIdentityUser user, string password)
{
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
// TODO: I don't think we really want to do this because we want to migrate all old password formats
// to the new format, not keep using it. AFAIK this block should be removed along with the code within the
// LegacyPasswordSecurity except the part that just validates old ones.
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
return _legacyPasswordSecurity.HashPasswordForStorage(password);
// We will explicitly detect names here since this allows us to future proof these checks.
// The default is PBKDF2.ASPNETCORE.V3:
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
// The PBKDF2.ASPNETCORE.V2 settings are:
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
switch (deserialized.HashAlgorithm)
{
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
return base.HashPassword(user, password);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var v2Hasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
return v2Hasher.HashPassword(user, password);
}
}
// else keep the default
// Always use the latest/current hash algorithm when hashing new passwords for storage.
// NOTE: This is only overridden to show that we can since we may need to adjust this in the future
// if new/different formats are required.
return base.HashPassword(user, password);
}
/// <summary>
/// Verifies a user's hashed password
/// </summary>
/// <param name="user"></param>
/// <param name="hashedPassword"></param>
/// <param name="providedPassword"></param>
/// <returns></returns>
/// <remarks>
/// This will check the user's current hashed password format stored with their user row and use that to verify the hash. This could be any hashes
/// from the very old v4, to the older v6-v8, to the older aspnet identity and finally to the most recent
/// </remarks>
public override PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
{
if (!user.PasswordConfig.IsNullOrWhiteSpace())
@@ -65,21 +55,29 @@ namespace Umbraco.Web.BackOffice.Security
{
var result = _legacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
return result
? PasswordVerificationResult.Success
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Failed;
}
// We will explicitly detect names here
// The default is PBKDF2.ASPNETCORE.V3:
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
// The PBKDF2.ASPNETCORE.V2 settings are:
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
switch (deserialized.HashAlgorithm)
{
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var v2Hasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
return v2Hasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
if (legacyResult == PasswordVerificationResult.Success) return PasswordVerificationResult.SuccessRehashNeeded;
return legacyResult;
}
}
// else go the default
// else go the default (v3)
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}

View File

@@ -39,7 +39,7 @@ namespace Umbraco.Web.Common.Runtime
[ComposeAfter(typeof(CoreInitialComposer))]
public class AspNetCoreComposer : ComponentComposer<AspNetCoreComponent>, IComposer
{
public new void Compose(Composition composition)
public override void Compose(Composition composition)
{
base.Compose(composition);
@@ -99,7 +99,7 @@ namespace Umbraco.Web.Common.Runtime
composition.RegisterUnique<ITemplateRenderer, TemplateRenderer>();
composition.RegisterUnique<IPublicAccessChecker, PublicAccessChecker>();
composition.RegisterUnique(factory => new LegacyPasswordSecurity(factory.GetInstance<IOptions<UserPasswordConfigurationSettings>>().Value));
composition.RegisterUnique(factory => new LegacyPasswordSecurity());
}
}
}

View File

@@ -281,7 +281,7 @@ function dependencies() {
var assetsTask = gulp.src(config.sources.globs.assets, { allowEmpty: true });
assetsTask = assetsTask.pipe(imagemin([
imagemin.gifsicle({interlaced: true}),
imagemin.jpegtran({progressive: true}),
imagemin.mozjpeg({progressive: true}),
imagemin.optipng({optimizationLevel: 5}),
imagemin.svgo({
plugins: [

View File

@@ -61,7 +61,7 @@
"gulp-cli": "^2.3.0",
"gulp-concat": "2.6.1",
"gulp-eslint": "6.0.0",
"gulp-imagemin": "6.1.1",
"gulp-imagemin": "7.1.0",
"gulp-less": "4.0.1",
"gulp-notify": "^3.0.0",
"gulp-postcss": "8.0.0",

View File

@@ -80,7 +80,7 @@ namespace Umbraco.Web.Security.Providers
CustomHashAlgorithmType.IsNullOrWhiteSpace() ? Membership.HashAlgorithmType : CustomHashAlgorithmType,
MaxInvalidPasswordAttempts));
_passwordSecurity = new Lazy<LegacyPasswordSecurity>(() => new LegacyPasswordSecurity(PasswordConfiguration));
_passwordSecurity = new Lazy<LegacyPasswordSecurity>(() => new LegacyPasswordSecurity());
}

View File

@@ -85,7 +85,7 @@ namespace Umbraco.Web.Security.Providers
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, newPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(Membership.HashAlgorithmType, encodedPassword, salt);
m.LastPasswordChangeDate = DateTime.Now;
MemberService.Save(m);
@@ -148,7 +148,7 @@ namespace Umbraco.Web.Security.Providers
var member = MemberService.CreateWithIdentity(
username,
email,
PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt),
PasswordSecurity.FormatPasswordForStorage(Membership.HashAlgorithmType, encodedPassword, salt),
memberTypeAlias,
isApproved);
@@ -407,7 +407,7 @@ namespace Umbraco.Web.Security.Providers
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, generatedPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(Membership.HashAlgorithmType, encodedPassword, salt);
m.LastPasswordChangeDate = DateTime.Now;
MemberService.Save(m);