Gets password roll forward working
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>>();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user