Gettting password formats and hashing sorted, ensuring the password format on the user is used

This commit is contained in:
Shannon
2020-05-27 13:48:26 +10:00
parent ac57aeb066
commit e47f81efdc
39 changed files with 302 additions and 140 deletions

View File

@@ -30,7 +30,7 @@ namespace Umbraco.Configuration.Models
_configuration.GetValue(Prefix + "RequireUppercase", false);
public string HashAlgorithmType =>
_configuration.GetValue(Prefix + "HashAlgorithmType", "HMACSHA256");
_configuration.GetValue(Prefix + "HashAlgorithmType", Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName); // TODO: Need to change to current format when we do members
public int MaxFailedAccessAttemptsBeforeLockout =>
_configuration.GetValue(Prefix + "MaxFailedAccessAttemptsBeforeLockout", 5);

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Configuration.Models
_configuration.GetValue(Prefix + "RequireUppercase", false);
public string HashAlgorithmType =>
_configuration.GetValue(Prefix + "HashAlgorithmType", "HMACSHA256");
_configuration.GetValue(Prefix + "HashAlgorithmType", Constants.Security.AspNetCoreV3PasswordHashAlgorithmName);
public int MaxFailedAccessAttemptsBeforeLockout =>
_configuration.GetValue(Prefix + "MaxFailedAccessAttemptsBeforeLockout", 5);

View File

@@ -22,7 +22,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("useLegacyEncoding", DefaultValue = "false")]
public bool UseLegacyEncoding => (bool)base["useLegacyEncoding"];
[ConfigurationProperty("hashAlgorithmType", DefaultValue = "HMACSHA256")]
[ConfigurationProperty("hashAlgorithmType", DefaultValue = Constants.Security.AspNetCoreV3PasswordHashAlgorithmName)]
public string HashAlgorithmType => (string)base["hashAlgorithmType"];
[ConfigurationProperty("maxFailedAccessAttemptsBeforeLockout", DefaultValue = "5")]

View File

@@ -22,6 +22,7 @@ namespace Umbraco.Core.BackOffice
private string _name;
private int _accessFailedCount;
private string _passwordHash;
private string _passwordConfig;
private string _culture;
private ObservableCollection<IIdentityUserLogin> _logins;
private Lazy<IEnumerable<IIdentityUserLogin>> _getLogins;
@@ -174,6 +175,12 @@ namespace Umbraco.Core.BackOffice
set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordHash, nameof(PasswordHash));
}
public string PasswordConfig
{
get => _passwordConfig;
set => _beingDirty.SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfig));
}
/// <summary>
/// Content start nodes assigned to the User (not ones assigned to the user's groups)

View File

@@ -57,6 +57,7 @@ namespace Umbraco.Core.BackOffice
target.Name = source.Name;
target.AccessFailedCount = source.FailedPasswordAttempts;
target.PasswordHash = GetPasswordHash(source.RawPasswordValue);
target.PasswordConfig = source.PasswordConfiguration;
target.StartContentIds = source.StartContentIds;
target.StartMediaIds = source.StartMediaIds;
target.Culture = source.GetUserCulture(_textService, _globalSettings).ToString(); // project CultureInfo to string

View File

@@ -53,6 +53,9 @@
public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp";
public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid";
public const string AspNetCoreV3PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V3";
public const string AspNetCoreV2PasswordHashAlgorithmName = "PBKDF2.ASPNETCORE.V2";
public const string AspNetUmbraco8PasswordHashAlgorithmName = "HMACSHA256";
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Umbraco.Web.HealthCheck
/// <summary>
/// Provides a base class for health checks.
/// </summary>
[DataContract(Name = "healtCheck", Namespace = "")]
[DataContract(Name = "healthCheck", Namespace = "")]
public abstract class HealthCheck : IDiscoverable
{
protected HealthCheck()

View File

@@ -4,7 +4,7 @@ using System.Runtime.Serialization;
namespace Umbraco.Web.HealthCheck
{
[DataContract(Name = "healtCheckAction", Namespace = "")]
[DataContract(Name = "healthCheckAction", Namespace = "")]
public class HealthCheckAction
{
/// <summary>

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Web.HealthCheck
/// The status returned for a health check when it performs it check
/// TODO: This model will be used in the WebApi result so needs attributes for JSON usage
/// </summary>
[DataContract(Name = "healtCheckStatus", Namespace = "")]
[DataContract(Name = "healthCheckStatus", Namespace = "")]
public class HealthCheckStatus
{
public HealthCheckStatus(string message)

View File

@@ -18,6 +18,7 @@ namespace Umbraco.Core.Models
private string _username;
private string _email;
private string _rawPasswordValue;
private string _passwordConfig;
/// <summary>
/// Constructor for creating an empty Member object
@@ -159,6 +160,13 @@ namespace Umbraco.Core.Models
}
}
[IgnoreDataMember]
public string PasswordConfiguration
{
get => _passwordConfig;
set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration));
}
/// <summary>
/// Gets or sets the Groups that Member is part of
/// </summary>
@@ -516,6 +524,6 @@ namespace Umbraco.Core.Models
/// <inheritdoc />
[IgnoreDataMember]
public bool HasAdditionalData => _additionalData != null;
public bool HasAdditionalData => _additionalData != null;
}
}

View File

@@ -16,6 +16,11 @@ namespace Umbraco.Core.Models.Membership
/// </summary>
string RawPasswordValue { get; set; }
/// <summary>
/// The user's specific password config (i.e. algorithm type, etc...)
/// </summary>
string PasswordConfiguration { get; set; }
string Comments { get; set; }
bool IsApproved { get; set; }
bool IsLockedOut { get; set; }

View File

@@ -5,6 +5,7 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Membership
{
/// <summary>
/// Defines the interface for a <see cref="User"/>
/// </summary>

View File

@@ -41,10 +41,10 @@ namespace Umbraco.Core.Models.Membership
public User(IGlobalSettings globalSettings, string name, string email, string username, string rawPasswordValue)
: this(globalSettings)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value cannot be null or whitespace.", "email");
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username");
if (string.IsNullOrEmpty(rawPasswordValue)) throw new ArgumentException("Value cannot be null or empty.", "rawPasswordValue");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(email));
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
if (string.IsNullOrEmpty(rawPasswordValue)) throw new ArgumentException("Value cannot be null or empty.", nameof(rawPasswordValue));
_name = name;
_email = email;
@@ -65,25 +65,29 @@ namespace Umbraco.Core.Models.Membership
/// <param name="email"></param>
/// <param name="username"></param>
/// <param name="rawPasswordValue"></param>
/// <param name="passwordConfig"></param>
/// <param name="userGroups"></param>
/// <param name="startContentIds"></param>
/// <param name="startMediaIds"></param>
public User(IGlobalSettings globalSettings, int id, string name, string email, string username, string rawPasswordValue, IEnumerable<IReadOnlyUserGroup> userGroups, int[] startContentIds, int[] startMediaIds)
public User(IGlobalSettings globalSettings, int id, string name, string email, string username,
string rawPasswordValue, string passwordConfig,
IEnumerable<IReadOnlyUserGroup> userGroups, int[] startContentIds, int[] startMediaIds)
: this(globalSettings)
{
//we allow whitespace for this value so just check null
if (rawPasswordValue == null) throw new ArgumentNullException("rawPasswordValue");
if (userGroups == null) throw new ArgumentNullException("userGroups");
if (startContentIds == null) throw new ArgumentNullException("startContentIds");
if (startMediaIds == null) throw new ArgumentNullException("startMediaIds");
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", "name");
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", "username");
if (rawPasswordValue == null) throw new ArgumentNullException(nameof(rawPasswordValue));
if (userGroups == null) throw new ArgumentNullException(nameof(userGroups));
if (startContentIds == null) throw new ArgumentNullException(nameof(startContentIds));
if (startMediaIds == null) throw new ArgumentNullException(nameof(startMediaIds));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name));
if (string.IsNullOrWhiteSpace(username)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(username));
Id = id;
_name = name;
_email = email;
_username = username;
_rawPasswordValue = rawPasswordValue;
_passwordConfig = passwordConfig;
_userGroups = new HashSet<IReadOnlyUserGroup>(userGroups);
_isApproved = true;
_isLockedOut = false;
@@ -105,6 +109,7 @@ namespace Umbraco.Core.Models.Membership
private DateTime? _invitedDate;
private string _email;
private string _rawPasswordValue;
private string _passwordConfig;
private IEnumerable<string> _allowedSections;
private HashSet<IReadOnlyUserGroup> _userGroups;
private bool _isApproved;
@@ -147,13 +152,21 @@ namespace Umbraco.Core.Models.Membership
get => _email;
set => SetPropertyValueAndDetectChanges(value, ref _email, nameof(Email));
}
[DataMember]
[IgnoreDataMember]
public string RawPasswordValue
{
get => _rawPasswordValue;
set => SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, nameof(RawPasswordValue));
}
[IgnoreDataMember]
public string PasswordConfiguration
{
get => _passwordConfig;
set => SetPropertyValueAndDetectChanges(value, ref _passwordConfig, nameof(PasswordConfiguration));
}
[DataMember]
public bool IsApproved
{

View File

@@ -0,0 +1,22 @@
using System.Runtime.Serialization;
namespace Umbraco.Core.Models.Membership
{
/// <summary>
/// The data stored against the user for their password configuration
/// </summary>
[DataContract(Name = "userPasswordSettings", Namespace = "")]
public class UserPasswordSettings
{
/// <summary>
/// The algorithm name
/// </summary>
/// <remarks>
/// This doesn't explicitly need to map to a 'true' algorithm name, this may match an algorithm name alias that
/// uses many different options such as PBKDF2.ASPNETCORE.V3 which would map to the aspnetcore's v3 implementation of PBKDF2
/// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
/// </remarks>
[DataMember(Name = "hashAlgorithm")]
public string HashAlgorithm { get; set; }
}
}

View File

@@ -1,20 +1,24 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Configuration;
namespace Umbraco.Core.Security
{
/// <summary>
/// Handles password hashing and formatting
/// </summary>
public class PasswordSecurity
{
public IPasswordConfiguration PasswordConfiguration { get; }
public PasswordGenerator _generator;
public ConfiguredPasswordValidator _validator;
// TODO: This class could/should be renamed since it's really purely about legacy hashing, we want to use the new hashing available
// to us but this is here for compatibility purposes.
// 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
@@ -22,31 +26,11 @@ namespace Umbraco.Core.Security
/// <param name="passwordConfiguration"></param>
public PasswordSecurity(IPasswordConfiguration passwordConfiguration)
{
PasswordConfiguration = passwordConfiguration;
_passwordConfiguration = passwordConfiguration;
_generator = new PasswordGenerator(passwordConfiguration);
}
/// <summary>
/// Checks if the password passes validation rules
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
public async Task<Attempt<IEnumerable<string>>> IsValidPasswordAsync(string password)
{
if (_validator == null)
_validator = new ConfiguredPasswordValidator(PasswordConfiguration);
var result = await _validator.ValidateAsync(password);
if (result.Succeeded)
return Attempt<IEnumerable<string>>.Succeed();
return Attempt<IEnumerable<string>>.Fail(result.Errors);
}
public string GeneratePassword()
{
if (_generator == null)
_generator = new PasswordGenerator(PasswordConfiguration);
return _generator.GeneratePassword();
}
public string GeneratePassword() => _generator.GeneratePassword();
/// <summary>
/// Returns a hashed password value used to store in a data store
@@ -89,7 +73,7 @@ namespace Umbraco.Core.Security
var saltBytes = Convert.FromBase64String(salt);
byte[] inArray;
var hashAlgorithm = GetHashAlgorithm(pass);
var hashAlgorithm = GetCurrentHashAlgorithm();
var algorithm = hashAlgorithm as KeyedHashAlgorithm;
if (algorithm != null)
{
@@ -185,37 +169,30 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Return the hash algorithm to use
/// Return the hash algorithm to use based on the <see cref="IPasswordConfiguration"/>
/// </summary>
/// <param name="password"></param>
/// <returns></returns>
public HashAlgorithm GetHashAlgorithm(string password)
private HashAlgorithm GetCurrentHashAlgorithm()
{
if (PasswordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace())
if (_passwordConfiguration.HashAlgorithmType.IsNullOrWhiteSpace())
throw new InvalidOperationException("No hash algorithm type specified");
var alg = HashAlgorithm.Create(PasswordConfiguration.HashAlgorithmType);
var alg = HashAlgorithm.Create(_passwordConfiguration.HashAlgorithmType);
if (alg == null)
throw new InvalidOperationException($"The hash algorithm specified {PasswordConfiguration.HashAlgorithmType} cannot be resolved");
throw new InvalidOperationException($"The hash algorithm specified {_passwordConfiguration.HashAlgorithmType} cannot be resolved");
return alg;
}
/// <summary>
/// Encodes the password.
/// </summary>
/// <param name="password">The password.</param>
/// <returns>The encoded password.</returns>
private string LegacyEncodePassword(string password)
public bool SupportHashAlgorithm(string algorithm)
{
var hashAlgorith = GetHashAlgorithm(password);
var encodedPassword = Convert.ToBase64String(hashAlgorith.ComputeHash(Encoding.Unicode.GetBytes(password)));
return encodedPassword;
if (algorithm.InvariantEquals(typeof(HMACSHA256).Name))
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;
}
}
}

View File

@@ -45,7 +45,6 @@ namespace Umbraco.Core.BackOffice
if (userService == null) throw new ArgumentNullException("userService");
if (externalLoginService == null) throw new ArgumentNullException("externalLoginService");
_mapper = mapper;
_userService = userService;
_externalLoginService = externalLoginService;
}
@@ -245,6 +244,7 @@ namespace Umbraco.Core.BackOffice
if (string.IsNullOrEmpty(passwordHash)) throw new ArgumentException("Value can't be empty.", nameof(passwordHash));
user.PasswordHash = passwordHash;
// TODO: Need to set the user.PasswordConfig based on what the current configuration is
return Task.CompletedTask;
}
@@ -820,6 +820,7 @@ namespace Umbraco.Core.BackOffice
{
anythingChanged = true;
user.RawPasswordValue = identityUser.PasswordHash;
user.PasswordConfiguration = identityUser.PasswordConfig;
}
if (identityUser.IsPropertyDirty("Culture")

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories
{
var guidId = dto.Id.ToGuid();
var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login,dto.Password,
var user = new User(globalSettings, dto.Id, dto.UserName, dto.Email, dto.Login, dto.Password, dto.PasswordConfig,
dto.UserGroupDtos.Select(x => ToReadOnlyGroup(x)).ToArray(),
dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Content).Select(x => x.StartNode).ToArray(),
dto.UserStartNodeDtos.Where(x => x.StartNodeType == (int)UserStartNodeDto.StartNodeTypeValue.Media).Select(x => x.StartNode).ToArray());
@@ -64,6 +64,7 @@ namespace Umbraco.Core.Persistence.Factories
Login = entity.Username,
NoConsole = entity.IsLockedOut,
Password = entity.RawPasswordValue,
PasswordConfig = entity.PasswordConfiguration,
UserLanguage = entity.Language,
UserName = entity.Name,
SecurityStampToken = entity.SecurityStamp,

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using Newtonsoft.Json;
using NPoco;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
@@ -15,6 +14,7 @@ using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using Umbraco.Core.Serialization;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -26,6 +26,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private readonly IMapperCollection _mapperCollection;
private readonly IGlobalSettings _globalSettings;
private readonly IUserPasswordConfiguration _passwordConfiguration;
private readonly IJsonSerializer _jsonSerializer;
private string _passwordConfigJson;
private bool _passwordConfigInitialized;
@@ -39,26 +40,31 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
/// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read.
/// </param>
/// <param name="globalSettings"></param>
public UserRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IMapperCollection mapperCollection, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration)
public UserRepository(IScopeAccessor scopeAccessor, AppCaches appCaches, ILogger logger, IMapperCollection mapperCollection, IGlobalSettings globalSettings, IUserPasswordConfiguration passwordConfiguration, IJsonSerializer jsonSerializer)
: base(scopeAccessor, appCaches, logger)
{
_mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection));
_globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings));
_passwordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration));
_jsonSerializer = jsonSerializer;
}
/// <summary>
/// Returns a serialized dictionary of the password configuration that is stored against the user in the database
/// </summary>
private string PasswordConfigJson
private string DefaultPasswordConfigJson
{
get
{
if (_passwordConfigInitialized)
return _passwordConfigJson;
var passwordConfig = new Dictionary<string, string> { { "hashAlgorithm", _passwordConfiguration.HashAlgorithmType } };
_passwordConfigJson = passwordConfig == null ? null : JsonConvert.SerializeObject(passwordConfig);
var passwordConfig = new UserPasswordSettings
{
HashAlgorithm = _passwordConfiguration.HashAlgorithmType
};
_passwordConfigJson = passwordConfig == null ? null : _jsonSerializer.Serialize(passwordConfig);
_passwordConfigInitialized = true;
return _passwordConfigJson;
}
@@ -438,10 +444,8 @@ ORDER BY colName";
var userDto = UserFactory.BuildDto(entity);
// check if we have a known config, we only want to store config for hashing
// TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
if (PasswordConfigJson != null)
userDto.PasswordConfig = PasswordConfigJson;
// check if we have a user config else use the default
userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson;
var id = Convert.ToInt32(Database.Insert(userDto));
entity.Id = id;
@@ -534,13 +538,9 @@ ORDER BY colName";
changedCols.Add("securityStampToken");
}
// check if we have a known config, we only want to store config for hashing
// TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
if (PasswordConfigJson != null)
{
userDto.PasswordConfig = PasswordConfigJson;
changedCols.Add("passwordConfig");
}
// check if we have a user config else use the default
userDto.PasswordConfig = entity.PasswordConfiguration ?? DefaultPasswordConfigJson;
changedCols.Add("passwordConfig");
}
// If userlogin or the email has changed then need to reset security stamp

View File

@@ -5,5 +5,7 @@
string Username { get; set; }
string RawPasswordValue { get; set; }
string PasswordConfig { get; set; }
}
}

View File

@@ -34,6 +34,7 @@ namespace Umbraco.Tests.Common.Builders
private string _path;
private string _username;
private string _rawPasswordValue;
private string _passwordConfig;
private string _email;
private int? _failedPasswordAttempts;
private bool? _isApproved;
@@ -237,6 +238,12 @@ namespace Umbraco.Tests.Common.Builders
set => _rawPasswordValue = value;
}
string IWithLoginBuilder.PasswordConfig
{
get => _passwordConfig;
set => _passwordConfig = value;
}
string IWithEmailBuilder.Email
{
get => _email;

View File

@@ -27,6 +27,7 @@ namespace Umbraco.Tests.Common.Builders
private string _name;
private string _username;
private string _rawPasswordValue;
private string _passwordConfig;
private string _email;
private int? _failedPasswordAttempts;
private bool? _isApproved;
@@ -183,6 +184,12 @@ namespace Umbraco.Tests.Common.Builders
set => _rawPasswordValue = value;
}
string IWithLoginBuilder.PasswordConfig
{
get => _passwordConfig;
set => _passwordConfig = value;
}
string IWithEmailBuilder.Email
{
get => _email;

View File

@@ -11,6 +11,7 @@ using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Scoping;
using Umbraco.Core.Serialization;
using Umbraco.Tests.Common.Builders.Extensions;
using Umbraco.Tests.Integration.Testing;
using Umbraco.Tests.Testing;
@@ -24,7 +25,7 @@ namespace Umbraco.Tests.Persistence.Repositories
private UserRepository CreateRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor) provider;
var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, GlobalSettings, Mock.Of<IUserPasswordConfiguration>());
var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, GlobalSettings, Mock.Of<IUserPasswordConfiguration>(), new JsonNetSerializer());
return repository;
}
@@ -116,7 +117,7 @@ namespace Umbraco.Tests.Persistence.Repositories
var id = user.Id;
var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of<IMapperCollection>(),GlobalSettings, Mock.Of<IUserPasswordConfiguration>());
var repository2 = new UserRepository((IScopeAccessor) provider, AppCaches.Disabled, Logger, Mock.Of<IMapperCollection>(),GlobalSettings, Mock.Of<IUserPasswordConfiguration>(), new JsonNetSerializer());
repository2.Delete(user);

View File

@@ -1,4 +1,5 @@
using NUnit.Framework;
using Umbraco.Core;
namespace Umbraco.Tests.Integration.Umbraco.Configuration.UmbracoSettings
{
@@ -68,7 +69,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Configuration.UmbracoSettings
[Test]
public void UserPasswordConfiguration_HashAlgorithmType()
{
Assert.IsTrue(UserPasswordConfiguration.HashAlgorithmType == "HMACSHA256");
Assert.IsTrue(UserPasswordConfiguration.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
}
[Test]
@@ -110,7 +111,7 @@ namespace Umbraco.Tests.Integration.Umbraco.Configuration.UmbracoSettings
[Test]
public void MemberPasswordConfiguration_HashAlgorithmType()
{
Assert.IsTrue(MemberPasswordConfiguration.HashAlgorithmType == "HMACSHA256");
Assert.IsTrue(MemberPasswordConfiguration.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
}
[Test]

View File

@@ -1,26 +1,15 @@
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
namespace Umbraco.Tests.Security
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Security
{
[TestFixture]
public class PasswordSecurityTests
{
[Test]
public void Get_Hash_Algorithm_Default()
{
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "HMACSHA256"));
var alg = passwordSecurity.GetHashAlgorithm("blah"); // not resolved
Assert.IsTrue(alg is HMACSHA256);
}
[Test]
public void Check_Password_Hashed_Non_KeyedHashAlgorithm()
@@ -40,7 +29,7 @@ namespace Umbraco.Tests.Security
[Test]
public void Check_Password_Hashed_KeyedHashAlgorithm()
{
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "HMACSHA256"));
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
string salt;
var pass = "ThisIsAHashedPassword";
@@ -55,7 +44,7 @@ namespace Umbraco.Tests.Security
[Test]
public void Format_Pass_For_Storage_Hashed()
{
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "HMACSHA256"));
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var salt = PasswordSecurity.GenerateSalt();
var stored = "ThisIsAHashedPassword";
@@ -68,7 +57,7 @@ namespace Umbraco.Tests.Security
[Test]
public void Get_Stored_Password_Hashed()
{
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == "HMACSHA256"));
var passwordSecurity = new PasswordSecurity(Mock.Of<IPasswordConfiguration>(x => x.HashAlgorithmType == Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName));
var salt = PasswordSecurity.GenerateSalt();
var stored = salt + "ThisIsAHashedPassword";
@@ -91,9 +80,7 @@ namespace Umbraco.Tests.Security
var result = PasswordSecurity.GenerateSalt();
if (i > 0)
{
Assert.AreEqual(lastLength, result.Length);
}
lastLength = result.Length;
}

View File

@@ -97,7 +97,7 @@ namespace Umbraco.Tests.Membership
.Returns(() => createdMember);
var provider = new MembersMembershipProvider(membershipServiceMock.Object, memberTypeServiceMock.Object, TestHelper.GetUmbracoVersion(), TestHelper.GetHostingEnvironment(), TestHelper.GetIpResolver());
provider.Initialize("test", new NameValueCollection { { "passwordFormat", "Hashed" }, { "hashAlgorithmType", "HMACSHA256" } });
provider.Initialize("test", new NameValueCollection { { "passwordFormat", "Hashed" }, { "hashAlgorithmType", Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName } });
MembershipCreateStatus status;

View File

@@ -14,6 +14,7 @@ using Umbraco.Tests.Testing;
using Umbraco.Core.PropertyEditors;
using System;
using Umbraco.Core.Configuration;
using Umbraco.Core.Serialization;
namespace Umbraco.Tests.Persistence.Repositories
{
@@ -67,7 +68,7 @@ namespace Umbraco.Tests.Persistence.Repositories
private UserRepository CreateRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor) provider;
var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, TestObjects.GetGlobalSettings(), Mock.Of<IUserPasswordConfiguration>());
var repository = new UserRepository(accessor, AppCaches.Disabled, Logger, Mappers, TestObjects.GetGlobalSettings(), Mock.Of<IUserPasswordConfiguration>(), new JsonNetSerializer());
return repository;
}

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Owin.Security.DataProtection;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership;
@@ -30,7 +31,7 @@ namespace Umbraco.Tests.Security
mockDataProtectionProvider.Setup(x => x.Create(It.IsAny<string>()))
.Returns(new Mock<IDataProtector>().Object);
mockPasswordConfiguration.Setup(x => x.HashAlgorithmType)
.Returns("HMACSHA256");
.Returns(Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName);
var userManager = BackOfficeOwinUserManager.Create(
mockPasswordConfiguration.Object,

View File

@@ -1,4 +1,5 @@
using Umbraco.Core.Configuration;
using Umbraco.Core;
namespace Umbraco.Tests.TestHelpers.Stubs
{
@@ -16,7 +17,7 @@ namespace Umbraco.Tests.TestHelpers.Stubs
public bool UseLegacyEncoding => false;
public string HashAlgorithmType => "HMACSHA256";
public string HashAlgorithmType => Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName;
public int MaxFailedAccessAttemptsBeforeLockout => 5;
}

View File

@@ -163,7 +163,6 @@
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Security\PasswordSecurityTests.cs" />
<Compile Include="Services\AmbiguousEventTests.cs" />
<Compile Include="Services\ContentServiceEventTests.cs" />
<Compile Include="Services\ContentServicePublishBranchTests.cs" />

View File

@@ -59,9 +59,9 @@ namespace Umbraco.Tests.Web.Controllers
var userServiceMock = new Mock<IUserService>();
userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", null, new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
userServiceMock.Setup(x => x.GetProfileById(It.IsAny<int>()))
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", null, new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
userServiceMock.Setup(service => service.GetPermissionsForPath(It.IsAny<IUser>(), It.IsAny<string>()))
.Returns(new EntityPermissionSet(123, new EntityPermissionCollection(new[]
{

View File

@@ -86,7 +86,7 @@ namespace Umbraco.Tests.Web.Controllers
userServiceMock.Setup(service => service.GetUserGroupsByAlias(It.IsAny<string[]>()))
.Returns(new[] { Mock.Of<IUserGroup>(group => group.Id == 123 && group.Alias == "writers" && group.Name == "Writers") });
userServiceMock.Setup(service => service.GetUserById(It.IsAny<int>()))
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
.Returns((int id) => id == 1234 ? new User(TestObjects.GetGlobalSettings(), 1234, "Test", "test@test.com", "test@test.com", "", null, new List<IReadOnlyUserGroup>(), new int[0], new int[0]) : null);
var usersController = new UsersController(
Factory.GetInstance<IGlobalSettings>(),

View File

@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SixLabors.ImageSharp.Web.DependencyInjection;
using Umbraco.Web.BackOffice.Routing;
using Microsoft.AspNetCore.Builder;
namespace Umbraco.Extensions
{

View File

@@ -3,6 +3,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Security;
using Umbraco.Core.Serialization;
using Umbraco.Net;
using Umbraco.Web.BackOffice.Security;
using Umbraco.Web.Common.AspNetCore;
@@ -34,6 +37,7 @@ namespace Umbraco.Extensions
.AddDefaultTokenProviders()
.AddUserStore<BackOfficeUserStore>()
.AddUserManager<BackOfficeUserManager>()
.AddSignInManager<BackOfficeSignInManager>()
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory<BackOfficeIdentityUser>>();
// Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance
@@ -53,15 +57,20 @@ namespace Umbraco.Extensions
// able to configure IdentityOptions to a specific provider since there is no named options. So we have strongly typed options
// and strongly typed ILookupNormalizer and IdentityErrorDescriber since those are 'global' and we need to be unintrusive.
// TODO: Could move all of this to BackOfficeComposer?
// Services used by identity
services.TryAddScoped<IUserValidator<BackOfficeIdentityUser>, UserValidator<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordValidator<BackOfficeIdentityUser>, PasswordValidator<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordHasher<BackOfficeIdentityUser>, PasswordHasher<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordHasher<BackOfficeIdentityUser>>(
services => new BackOfficePasswordHasher(
new PasswordSecurity(services.GetRequiredService<IUserPasswordConfiguration>()),
services.GetRequiredService<IJsonSerializer>()));
services.TryAddScoped<IUserConfirmation<BackOfficeIdentityUser>, DefaultUserConfirmation<BackOfficeIdentityUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<BackOfficeIdentityUser>, UserClaimsPrincipalFactory<BackOfficeIdentityUser>>();
services.TryAddScoped<UserManager<BackOfficeIdentityUser>>();
// CUSTOM:
// CUSTOM:
services.TryAddScoped<BackOfficeLookupNormalizer>();
services.TryAddScoped<BackOfficeIdentityErrorDescriber>();

View File

@@ -16,7 +16,6 @@ namespace Umbraco.Web.BackOffice.Runtime
{
composition.RegisterUnique<BackOfficeAreaRoutes>();
composition.RegisterUnique<BackOfficeServerVariables>();
composition.Register<BackOfficeSignInManager>(Lifetime.Scope);
composition.RegisterUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
}

View File

@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Identity;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Security;
using Umbraco.Core;
using Umbraco.Core.Models.Membership;
using Microsoft.Extensions.Options;
using Umbraco.Core.Serialization;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// A password hasher for back office users
/// </summary>
public class BackOfficePasswordHasher : PasswordHasher<BackOfficeIdentityUser>
{
private readonly PasswordSecurity _passwordSecurity;
private readonly IJsonSerializer _jsonSerializer;
public BackOfficePasswordHasher(PasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
{
_passwordSecurity = passwordSecurity;
_jsonSerializer = jsonSerializer;
}
public override string HashPassword(BackOfficeIdentityUser user, string password)
{
if (!user.PasswordConfig.IsNullOrWhiteSpace())
{
// check if the (legacy) password security supports this hash algorith and if so then use it
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
if (_passwordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
return _passwordSecurity.HashPasswordForStorage(password);
// We will explicitly detect names here since this allows us to future proof these checks.
// The default is PBKDF2.ASPNETCORE.V3:
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
// The PBKDF2.ASPNETCORE.V2 settings are:
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
switch (deserialized.HashAlgorithm)
{
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
return base.HashPassword(user, password);
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
var v2Hasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
return v2Hasher.HashPassword(user, password);
}
}
// else keep the default
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);
}
}
// else go the default
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}
private class V2PasswordHasherOptions : IOptions<PasswordHasherOptions>
{
public PasswordHasherOptions Value => new PasswordHasherOptions
{
CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
};
}
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
@@ -29,12 +30,18 @@ namespace Umbraco.Web.BackOffice.Security
options.Lockout.AllowedForNewUsers = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30);
options.Password.RequiredLength = _userPasswordConfiguration.RequiredLength;
options.Password.RequireNonAlphanumeric = _userPasswordConfiguration.RequireNonLetterOrDigit;
options.Password.RequireDigit = _userPasswordConfiguration.RequireDigit;
options.Password.RequireLowercase = _userPasswordConfiguration.RequireLowercase;
options.Password.RequireUppercase = _userPasswordConfiguration.RequireUppercase;
ConfigurePasswordOptions(_userPasswordConfiguration, options.Password);
options.Lockout.MaxFailedAccessAttempts = _userPasswordConfiguration.MaxFailedAccessAttemptsBeforeLockout;
}
public static void ConfigurePasswordOptions(IPasswordConfiguration input, PasswordOptions output)
{
output.RequiredLength = input.RequiredLength;
output.RequireNonAlphanumeric = input.RequireNonLetterOrDigit;
output.RequireDigit = input.RequireDigit;
output.RequireLowercase = input.RequireLowercase;
output.RequireUppercase = input.RequireUppercase;
}
}
}

View File

@@ -31,7 +31,6 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
using Umbraco.Core.Strings;
using Umbraco.Core.Mapping;
using Umbraco.Web.Routing;
@@ -64,12 +63,14 @@ namespace Umbraco.Web.Editors
{
_passwordConfig = passwordConfig ?? throw new ArgumentNullException(nameof(passwordConfig));
_propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors));
_passwordSecurity = new PasswordSecurity(_passwordConfig);
_passwordValidator = new ConfiguredPasswordValidator();
}
private readonly IMemberPasswordConfiguration _passwordConfig;
private readonly PropertyEditorCollection _propertyEditors;
private PasswordSecurity _passwordSecurity;
private PasswordSecurity PasswordSecurity => _passwordSecurity ?? (_passwordSecurity = new PasswordSecurity(_passwordConfig));
private readonly PasswordSecurity _passwordSecurity;
private readonly IPasswordValidator _passwordValidator;
public PagedResult<MemberBasic> GetPagedResults(
int pageNumber = 1,
@@ -296,7 +297,7 @@ namespace Umbraco.Web.Editors
var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
{
CreatorId = Security.CurrentUser.Id,
RawPasswordValue = PasswordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
Comments = contentItem.Comments,
IsApproved = contentItem.IsApproved
};
@@ -358,7 +359,7 @@ namespace Umbraco.Web.Editors
return;
// 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)
@@ -383,7 +384,7 @@ namespace Umbraco.Web.Editors
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
{
var validPassword = await PasswordSecurity.IsValidPasswordAsync(contentItem.Password.NewPassword);
var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword);
if (!validPassword)
{
ModelState.AddPropertyError(

View File

@@ -1,20 +1,31 @@
using Microsoft.AspNet.Identity;
using System.Collections.Generic;
using System.Threading.Tasks;
using Umbraco.Core.Configuration;
namespace Umbraco.Core.Security
{
/// <summary>
/// Ensure that both the normal password validator rules are processed along with the underlying membership provider rules
/// </summary>
public class ConfiguredPasswordValidator : PasswordValidator
// NOTE: Migrated to netcore (in a different way)
public interface IPasswordValidator
{
public ConfiguredPasswordValidator(IPasswordConfiguration config)
Task<Attempt<IEnumerable<string>>> ValidateAsync(IPasswordConfiguration config, string password);
}
// NOTE: Migrated to netcore (in a different way)
public class ConfiguredPasswordValidator : PasswordValidator, IPasswordValidator
{
async Task<Attempt<IEnumerable<string>>> IPasswordValidator.ValidateAsync(IPasswordConfiguration passwordConfiguration, string password)
{
RequiredLength = config.RequiredLength;
RequireNonLetterOrDigit = config.RequireNonLetterOrDigit;
RequireDigit = config.RequireDigit;
RequireLowercase = config.RequireLowercase;
RequireUppercase = config.RequireUppercase;
RequiredLength = passwordConfiguration.RequiredLength;
RequireNonLetterOrDigit = passwordConfiguration.RequireNonLetterOrDigit;
RequireDigit = passwordConfiguration.RequireDigit;
RequireLowercase = passwordConfiguration.RequireLowercase;
RequireUppercase = passwordConfiguration.RequireUppercase;
var result = await ValidateAsync(password);
if (result.Succeeded)
return Attempt<IEnumerable<string>>.Succeed();
return Attempt<IEnumerable<string>>.Fail(result.Errors);
}
}
}

View File

@@ -199,7 +199,6 @@
<Compile Include="Security\MembershipProviderBase.cs" />
<Compile Include="Security\MembershipProviderExtensions.cs" />
<Compile Include="Security\OwinDataProtectorTokenProvider.cs" />
<Compile Include="Security\PasswordSecurity.cs" />
<Compile Include="Security\PublicAccessChecker.cs" />
<Compile Include="Security\UmbracoMembershipProviderBase.cs" />
<Compile Include="Security\UmbracoSecurityStampValidator.cs" />