Gettting password formats and hashing sorted, ensuring the password format on the user is used
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.Runtime.Serialization;
|
||||
|
||||
namespace Umbraco.Web.HealthCheck
|
||||
{
|
||||
[DataContract(Name = "healtCheckAction", Namespace = "")]
|
||||
[DataContract(Name = "healthCheckAction", Namespace = "")]
|
||||
public class HealthCheckAction
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -5,6 +5,7 @@ using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Models.Membership
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Defines the interface for a <see cref="User"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
22
src/Umbraco.Core/Models/Membership/UserPasswordSettings.cs
Normal file
22
src/Umbraco.Core/Models/Membership/UserPasswordSettings.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
string Username { get; set; }
|
||||
|
||||
string RawPasswordValue { get; set; }
|
||||
|
||||
string PasswordConfig { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -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>(),
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ namespace Umbraco.Web.BackOffice.Runtime
|
||||
{
|
||||
composition.RegisterUnique<BackOfficeAreaRoutes>();
|
||||
composition.RegisterUnique<BackOfficeServerVariables>();
|
||||
composition.Register<BackOfficeSignInManager>(Lifetime.Scope);
|
||||
|
||||
composition.RegisterUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user