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:
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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(createdMember.RawPasswordValue, out salt);
|
||||
var hashedPassword = provider.PasswordSecurity.HashPassword("testtest$1", salt);
|
||||
Assert.AreEqual(hashedPassword, storedPassword);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user