Merge pull request #9100 from umbraco/netcore/task/6964-legacy-password-format

Adds support for the super old password format so we can handle upgrades
This commit is contained in:
Mole
2020-10-07 11:25:51 +02:00
committed by GitHub
20 changed files with 302 additions and 642 deletions

View File

@@ -62,6 +62,7 @@
public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3";
public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2";
public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256";
public const string AspNetUmbraco4PasswordHashAlgorithmName = "HMACSHA1";
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Security.Cryptography;
using System.Text;
using Umbraco.Core.Configuration;
@@ -11,66 +12,115 @@ namespace Umbraco.Core.Security
/// </summary>
public class LegacyPasswordSecurity
{
// TODO: This class no longer has the logic available to verify the old old old password format, we should
// include this ability so that upgrades for very old versions/data can work and then auto-migrate to the new password format.
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>
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(password, out salt);
return FormatPasswordForStorage(hashed, salt);
var hashed = HashNewPassword(algorithmType, password, out salt);
return FormatPasswordForStorage(algorithmType, hashed, salt);
}
// Used for tests
[EditorBrowsable(EditorBrowsableState.Never)]
public string FormatPasswordForStorage(string algorithmType, string hashedPassword, string salt)
{
if (IsLegacySHA1Algorithm(algorithmType))
{
return hashedPassword;
}
return salt + hashedPassword;
}
/// <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.
/// Verifies if the password matches the expected hash+salt of the stored password string
/// </summary>
/// <param name="hashedPassword"></param>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="password">The password.</param>
/// <param name="dbPassword">The value of the password stored in a data store.</param>
/// <returns></returns>
public bool VerifyPassword(string algorithm, string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
return false;
var storedHashedPass = ParseStoredHashPassword(algorithm, dbPassword, out var salt);
var hashed = HashPassword(algorithm, password, salt);
return storedHashedPass == hashed;
}
/// <summary>
/// Create a new password hash and a new salt
/// </summary>
/// <param name="algorithm">The hashing algorithm for the password.</param>
/// <param name="newPassword"></param>
/// <param name="salt"></param>
/// <returns></returns>
public string FormatPasswordForStorage(string hashedPassword, string salt)
// TODO: Do we need this method? We shouldn't be using this class to create new password hashes for storage
public string HashNewPassword(string algorithm, string newPassword, out string salt)
{
return salt + hashedPassword;
salt = GenerateSalt();
return HashPassword(algorithm, newPassword, salt);
}
/// <summary>
/// Parses out the hashed password and the salt from the stored password string value
/// </summary>
/// <param name="algorithm">The hashing algorithm for the stored password.</param>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
public string ParseStoredHashPassword(string algorithm, string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
// This is for the <= v4 hashing algorithm for which there was no salt
if (IsLegacySHA1Algorithm(algorithm))
{
salt = string.Empty;
return storedString;
}
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
return storedString.Substring(saltLen.Length);
}
public static string GenerateSalt()
{
var numArray = new byte[16];
new RNGCryptoServiceProvider().GetBytes(numArray);
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>
public string HashPassword(string pass, string salt)
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 = GetCurrentHashAlgorithm();
var hashAlgorithm = GetHashAlgorithm(algorithmType);
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
@@ -113,83 +163,64 @@ namespace Umbraco.Core.Security
return Convert.ToBase64String(inArray);
}
/// <summary>
/// Verifies if the password matches the expected hash+salt of the stored password string
/// </summary>
/// <param name="password">The password.</param>
/// <param name="dbPassword">The value of the password stored in a data store.</param>
/// <returns></returns>
public bool VerifyPassword(string password, string dbPassword)
{
if (string.IsNullOrWhiteSpace(dbPassword)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(dbPassword));
if (dbPassword.StartsWith(Constants.Security.EmptyPasswordPrefix))
return false;
var storedHashedPass = ParseStoredHashPassword(dbPassword, out var salt);
var hashed = HashPassword(password, salt);
return storedHashedPass == hashed;
}
/// <summary>
/// Create a new password hash and a new salt
/// </summary>
/// <param name="newPassword"></param>
/// <param name="salt"></param>
/// <returns></returns>
public string HashNewPassword(string newPassword, out string salt)
{
salt = GenerateSalt();
return HashPassword(newPassword, salt);
}
/// <summary>
/// Parses out the hashed password and the salt from the stored password string value
/// </summary>
/// <param name="storedString"></param>
/// <param name="salt">returns the salt</param>
/// <returns></returns>
public string ParseStoredHashPassword(string storedString, out string salt)
{
if (string.IsNullOrWhiteSpace(storedString)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(storedString));
var saltLen = GenerateSalt();
salt = storedString.Substring(0, saltLen.Length);
return storedString.Substring(saltLen.Length);
}
public static string GenerateSalt()
{
var numArray = new byte[16];
new RNGCryptoServiceProvider().GetBytes(numArray);
return Convert.ToBase64String(numArray);
}
/// <summary>
/// Return the hash algorithm to use based on the <see cref="IPasswordConfiguration"/>
/// </summary>
/// <param name="algorithm">The hashing algorithm name.</param>
/// <param name="password"></param>
/// <returns></returns>
private HashAlgorithm GetCurrentHashAlgorithm()
private HashAlgorithm GetHashAlgorithm(string algorithm)
{
if (_passwordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace())
if (algorithm.IsNullOrWhiteSpace())
throw new InvalidOperationException("No hash algorithm type specified");
var alg = HashAlgorithm.Create(_passwordConfiguration.HashAlgorithmType);
var alg = HashAlgorithm.Create(algorithm);
if (alg == null)
throw new InvalidOperationException($"The hash algorithm specified {_passwordConfiguration.HashAlgorithmType} cannot be resolved");
throw new InvalidOperationException($"The hash algorithm specified {algorithm} cannot be resolved");
return alg;
}
public bool SupportHashAlgorithm(string algorithm)
{
if (algorithm.InvariantEquals(typeof(HMACSHA256).Name))
// This is for the v6-v8 hashing algorithm
if (algorithm.InvariantEquals(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName))
return true;
// This is for the <= v4 hashing algorithm
if (IsLegacySHA1Algorithm(algorithm))
return true;
// TODO: Need to add the old old old format in here too which was just HMACSHA1 IIRC but had a custom key implementation as the password itself
return false;
}
private bool IsLegacySHA1Algorithm(string algorithm) => algorithm.InvariantEquals(Constants.Security.AspNetUmbraco4PasswordHashAlgorithmName);
/// <summary>
/// Hashes the password with the old v4 algorithm
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The encoded password.</returns>
private string HashLegacySHA1Password(string password)
{
var hashAlgorithm = GetLegacySHA1Algorithm(password);
var hash = Convert.ToBase64String(hashAlgorithm.ComputeHash(Encoding.Unicode.GetBytes(password)));
return hash;
}
/// <summary>
/// Returns the old v4 algorithm and settings
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
private HashAlgorithm GetLegacySHA1Algorithm(string password)
{
return new HMACSHA1
{
//the legacy salt was actually the password :(
Key = Encoding.Unicode.GetBytes(password)
};
}
}
}

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

@@ -0,0 +1,109 @@
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
{
[TestFixture]
public class LegacyPasswordSecurityTests
{
[Test]
public void Check_Password_Hashed_Non_KeyedHashAlgorithm()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "SHA256");
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);
Assert.IsTrue(result);
}
[Test]
public void Check_Password_Hashed_KeyedHashAlgorithm()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
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);
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);
Assert.IsTrue(result);
}
[Test]
public void Format_Pass_For_Storage_Hashed()
{
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(passwordConfiguration.HashAlgorithmType, stored, salt);
Assert.AreEqual(salt + "ThisIsAHashedPassword", result);
}
[Test]
public void Get_Stored_Password_Hashed()
{
var passwordConfiguration = Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var passwordSecurity = new LegacyPasswordSecurity();
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = salt + "ThisIsAHashedPassword";
string initSalt;
var result = passwordSecurity.ParseStoredHashPassword(passwordConfiguration.HashAlgorithmType, stored, out initSalt);
Assert.AreEqual("ThisIsAHashedPassword", result);
}
/// <summary>
/// The salt generated is always the same length
/// </summary>
[Test]
public void Check_Salt_Length()
{
var lastLength = 0;
for (var i = 0; i < 10000; i++)
{
var result = LegacyPasswordSecurity.GenerateSalt();
if (i > 0)
Assert.AreEqual(lastLength, result.Length);
lastLength = result.Length;
}
}
}
}

View File

@@ -1,90 +0,0 @@
using Moq;
using NUnit.Framework;
using System.Security.Cryptography;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
{
[TestFixture]
public class PasswordSecurityTests
{
[Test]
public void Check_Password_Hashed_Non_KeyedHashAlgorithm()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "SHA256"));
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var result = passwordSecurity.VerifyPassword("ThisIsAHashedPassword", storedPassword);
Assert.IsTrue(result);
}
[Test]
public void Check_Password_Hashed_KeyedHashAlgorithm()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
string salt;
var pass = "ThisIsAHashedPassword";
var hashed = passwordSecurity.HashNewPassword(pass, out salt);
var storedPassword = passwordSecurity.FormatPasswordForStorage(hashed, salt);
var result = passwordSecurity.VerifyPassword("ThisIsAHashedPassword", storedPassword);
Assert.IsTrue(result);
}
[Test]
public void Format_Pass_For_Storage_Hashed()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = "ThisIsAHashedPassword";
var result = passwordSecurity.FormatPasswordForStorage(stored, salt);
Assert.AreEqual(salt + "ThisIsAHashedPassword", result);
}
[Test]
public void Get_Stored_Password_Hashed()
{
var passwordSecurity = new LegacyPasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var salt = LegacyPasswordSecurity.GenerateSalt();
var stored = salt + "ThisIsAHashedPassword";
string initSalt;
var result = passwordSecurity.ParseStoredHashPassword(stored, out initSalt);
Assert.AreEqual("ThisIsAHashedPassword", result);
}
/// <summary>
/// The salt generated is always the same length
/// </summary>
[Test]
public void Check_Salt_Length()
{
var lastLength = 0;
for (var i = 0; i < 10000; i++)
{
var result = LegacyPasswordSecurity.GenerateSalt();
if (i > 0)
Assert.AreEqual(lastLength, result.Length);
lastLength = result.Length;
}
}
}
}

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(createdMember.RawPasswordValue, out salt);
var hashedPassword = provider.PasswordSecurity.HashPassword("testtest$1", salt);
Assert.AreEqual(hashedPassword, storedPassword);
}
}
}

View File

@@ -1,69 +0,0 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Owin.Security.DataProtection;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Models.Membership;
using Umbraco.Net;
using Umbraco.Web.Security;
namespace Umbraco.Tests.Security
{
public class BackOfficeOwinUserManagerTests
{
[Test]
public async Task CheckPasswordAsync_When_Default_Password_Hasher_Validates_Umbraco7_Hash_Expect_Valid_Password()
{
const string v7Hash = "7Uob6fMTTxDIhWGebYiSxg==P+hgvWlXLbDd4cFLADn811KOaVI/9pg1PNvTuG5NklY=";
const string plaintext = "4XxzH3s3&J";
var userPasswordConfiguration = new UserPasswordConfigurationSettings()
{
HashAlgorithmType = Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName
};
var mockIpResolver = new Mock<IIpResolver>();
var mockUserStore = new Mock<IUserPasswordStore<BackOfficeIdentityUser>>();
var mockDataProtectionProvider = new Mock<IDataProtectionProvider>();
mockDataProtectionProvider.Setup(x => x.Create(It.IsAny<string>()))
.Returns(new Mock<IDataProtector>().Object);
var userManager = BackOfficeOwinUserManager.Create(
Options.Create(userPasswordConfiguration),
mockIpResolver.Object,
mockUserStore.Object,
null,
mockDataProtectionProvider.Object,
new NullLogger<UserManager<BackOfficeIdentityUser>>());
var globalSettings = new GlobalSettings()
{
DefaultUILanguage = "test"
};
var user = new BackOfficeIdentityUser(globalSettings, 2, new List<IReadOnlyUserGroup>())
{
UserName = "alice",
Name = "Alice",
Email = "alice@umbraco.test",
PasswordHash = v7Hash
};
mockUserStore.Setup(x => x.GetPasswordHashAsync(user, It.IsAny<CancellationToken>()))
.ReturnsAsync(v7Hash);
var isValidPassword = await userManager.CheckPasswordAsync(user, plaintext);
Assert.True(isValidPassword);
}
}
}

View File

@@ -150,7 +150,6 @@
<Compile Include="Routing\BaseUrlProviderTest.cs" />
<Compile Include="Routing\UrlProviderWithHideTopLevelNodeFromPathTests.cs" />
<Compile Include="Scoping\ScopeEventDispatcherTests.cs" />
<Compile Include="Security\BackOfficeOwinUserManagerTests.cs" />
<Compile Include="Security\OwinDataProtectorTokenProviderTests.cs" />
<Compile Include="Persistence\Repositories\UserRepositoryTest.cs" />
<Compile Include="TestHelpers\Entities\MockedEntity.cs" />
@@ -233,8 +232,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,72 +11,73 @@ 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 _passwordSecurity;
private readonly LegacyPasswordSecurity _legacyPasswordSecurity;
private readonly IJsonSerializer _jsonSerializer;
private readonly PasswordHasher<BackOfficeIdentityUser> _aspnetV2PasswordHasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
public BackOfficePasswordHasher(LegacyPasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
{
_passwordSecurity = passwordSecurity;
_legacyPasswordSecurity = passwordSecurity;
_jsonSerializer = jsonSerializer;
}
public override string HashPassword(BackOfficeIdentityUser user, string password)
{
// 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())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_passwordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
return _passwordSecurity.HashPasswordForStorage(password);
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
{
var result = _legacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
return result
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Failed;
}
// We will explicitly detect names here since this allows us to future proof these checks.
// 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.HashPassword(user, password);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var v2Hasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
return v2Hasher.HashPassword(user, password);
}
}
// else keep the default
return base.HashPassword(user, password);
}
public override PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
{
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_passwordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
{
var result = _passwordSecurity.VerifyPassword(providedPassword, hashedPassword);
return result
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
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

@@ -7,15 +7,14 @@ using Microsoft.Extensions.Options;
using Microsoft.Owin.Security.DataProtection;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Mapping;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Net;
namespace Umbraco.Web.Security
{
// TODO: Most of this is already migrated to netcore, there's probably not much more to go and then we can complete remove it
public class BackOfficeOwinUserManager : BackOfficeUserManager
{
public const string OwinMarkerKey = "Umbraco.Web.Security.Identity.BackOfficeUserManagerMarker";
@@ -118,11 +117,6 @@ namespace Umbraco.Web.Security
#endregion
protected override IPasswordHasher<BackOfficeIdentityUser> GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration)
{
return new UserAwarePasswordHasher<BackOfficeIdentityUser>(new LegacyPasswordSecurity(passwordConfiguration));
}
protected void InitUserManager(BackOfficeOwinUserManager manager, IDataProtectionProvider dataProtectionProvider)
{
// use a custom hasher based on our membership provider

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

@@ -83,9 +83,9 @@ namespace Umbraco.Web.Security.Providers
if (m == null) return false;
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(newPassword, out 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);
@@ -143,12 +143,12 @@ namespace Umbraco.Web.Security.Providers
}
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(password, out salt);
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, password, out salt);
var member = MemberService.CreateWithIdentity(
username,
email,
PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt),
PasswordSecurity.FormatPasswordForStorage(Membership.HashAlgorithmType, encodedPassword, salt),
memberTypeAlias,
isApproved);
@@ -406,8 +406,8 @@ namespace Umbraco.Web.Security.Providers
}
string salt;
var encodedPassword = PasswordSecurity.HashNewPassword(generatedPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(encodedPassword, salt);
var encodedPassword = PasswordSecurity.HashNewPassword(Membership.HashAlgorithmType, generatedPassword, out salt);
m.RawPasswordValue = PasswordSecurity.FormatPasswordForStorage(Membership.HashAlgorithmType, encodedPassword, salt);
m.LastPasswordChangeDate = DateTime.Now;
MemberService.Save(m);
@@ -519,7 +519,7 @@ namespace Umbraco.Web.Security.Providers
};
}
var authenticated = PasswordSecurity.VerifyPassword(password, member.RawPasswordValue);
var authenticated = PasswordSecurity.VerifyPassword(Membership.HashAlgorithmType, password, member.RawPasswordValue);
var requiresFullSave = false;

View File

@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Security;
namespace Umbraco.Web.Security
{
public class UserAwarePasswordHasher<T> : IPasswordHasher<T>
where T : BackOfficeIdentityUser
{
private readonly LegacyPasswordSecurity _passwordSecurity;
public UserAwarePasswordHasher(LegacyPasswordSecurity passwordSecurity)
{
_passwordSecurity = passwordSecurity;
}
public string HashPassword(string password)
{
return _passwordSecurity.HashPasswordForStorage(password);
}
public string HashPassword(T user, string password)
{
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return HashPassword(password);
}
public PasswordVerificationResult VerifyHashedPassword(T user, string hashedPassword, string providedPassword)
{
// TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return _passwordSecurity.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}

View File

@@ -194,7 +194,6 @@
<Compile Include="Security\OwinDataProtectorTokenProvider.cs" />
<Compile Include="Security\PublicAccessChecker.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UserAwarePasswordHasher.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="UmbracoContext.cs" />
<Compile Include="UmbracoContextFactory.cs" />