Implement password config storage for members (#10170)
* Getting new netcore PublicAccessChecker in place * Adds full test coverage for PublicAccessChecker * remove PublicAccessComposer * adjust namespaces, ensure RoleManager works, separate public access controller, reduce content controller * Implements the required methods on IMemberManager, removes old migrated code * Updates routing to be able to re-route, Fixes middleware ordering ensuring endpoints are last, refactors pipeline options, adds public access middleware, ensures public access follows all hops * adds note * adds note * Cleans up ext methods, ensures that members identity is added on both front-end and back ends. updates how UmbracoApplicationBuilder works in that it explicitly starts endpoints at the time of calling. * Changes name to IUmbracoEndpointBuilder * adds note * Fixing tests, fixing error describers so there's 2x one for back office, one for members, fixes TryConvertTo, fixes login redirect * fixing build * Updates user manager to correctly validate password hashing and injects the IBackOfficeUserPasswordChecker * Merges PR * Fixes up build and notes * Implements security stamp and email confirmed for members, cleans up a bunch of repo/service level member groups stuff, shares user store code between members and users and fixes the user identity object so we arent' tracking both groups and roles. * Security stamp for members is now working * Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates startup code to be more clear and removes magic that registers middleware. * adds note * removes unused filter, fixes build * fixes WebPath and tests * Looks up entities in one query * remove usings * Fix test, remove stylesheet * Set status code before we write to response to avoid error * Ensures that users and members are validated when logging in. Shares more code between users and members. * merge changes * oops * Reducing and removing published member cache * Fixes RepositoryCacheKeys to ensure the keys are normalized * oops didn't mean to commit this * Fix casing issues with caching, stop boxing value types for all cache operations, stop re-creating string keys in DefaultRepositoryCachePolicy * oops didn't mean to comit this * bah, far out this keeps getting recommitted. sorry * cannot inject IPublishedMemberCache and cannot have IPublishedMember * splits out files, fixes build * fix tests * removes membership provider classes * removes membership provider classes * updates the identity map definition * reverts commented out lines * reverts commented out lines * Implements members Password config in db, fixes members cookie auth to not interfere with the back office cookie auth, fixes Startup sequence, fixes startup pipeline * commits change to Startup * Rename migration from `MemberTableColumns2` to `AddPasswordConfigToMemberTable` * Fix test * Fix tests, but adding default passwordConfig to members Co-authored-by: Bjarke Berg <mail@bergmania.dk>
This commit is contained in:
@@ -1,93 +1,21 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
using Constants = Umbraco.Cms.Core.Constants;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A password hasher for back office users
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows us to verify passwords in old formats and roll forward to the latest format
|
||||
/// </remarks>
|
||||
public class BackOfficePasswordHasher : PasswordHasher<BackOfficeIdentityUser>
|
||||
public class BackOfficePasswordHasher : UmbracoPasswordHasher<BackOfficeIdentityUser>
|
||||
{
|
||||
private readonly LegacyPasswordSecurity _legacyPasswordSecurity;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly PasswordHasher<BackOfficeIdentityUser> _aspnetV2PasswordHasher = new PasswordHasher<BackOfficeIdentityUser>(new V2PasswordHasherOptions());
|
||||
|
||||
public BackOfficePasswordHasher(LegacyPasswordSecurity passwordSecurity, IJsonSerializer jsonSerializer)
|
||||
: base(passwordSecurity, jsonSerializer)
|
||||
{
|
||||
_legacyPasswordSecurity = passwordSecurity;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
}
|
||||
|
||||
public override string HashPassword(BackOfficeIdentityUser user, string password)
|
||||
{
|
||||
// Always use the latest/current hash algorithm when hashing new passwords for storage.
|
||||
// NOTE: This is only overridden to show that we can since we may need to adjust this in the future
|
||||
// if new/different formats are required.
|
||||
return base.HashPassword(user, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a user's hashed password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="hashedPassword"></param>
|
||||
/// <param name="providedPassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This will check the user's current hashed password format stored with their user row and use that to verify the hash. This could be any hashes
|
||||
/// from the very old v4, to the older v6-v8, to the older aspnet identity and finally to the most recent
|
||||
/// </remarks>
|
||||
public override PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
if (!user.PasswordConfig.IsNullOrWhiteSpace())
|
||||
{
|
||||
// check if the (legacy) password security supports this hash algorith and if so then use it
|
||||
var deserialized = _jsonSerializer.Deserialize<UserPasswordSettings>(user.PasswordConfig);
|
||||
if (_legacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
|
||||
{
|
||||
var result = _legacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
|
||||
return result
|
||||
? PasswordVerificationResult.SuccessRehashNeeded
|
||||
: PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
// We will explicitly detect names here
|
||||
// The default is PBKDF2.ASPNETCORE.V3:
|
||||
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
|
||||
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
|
||||
// The PBKDF2.ASPNETCORE.V2 settings are:
|
||||
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
|
||||
|
||||
switch (deserialized.HashAlgorithm)
|
||||
{
|
||||
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
|
||||
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
|
||||
var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
if (legacyResult == PasswordVerificationResult.Success)
|
||||
return PasswordVerificationResult.SuccessRehashNeeded;
|
||||
return legacyResult;
|
||||
}
|
||||
}
|
||||
|
||||
// else go the default (v3)
|
||||
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
}
|
||||
|
||||
private class V2PasswordHasherOptions : IOptions<PasswordHasherOptions>
|
||||
{
|
||||
public PasswordHasherOptions Value => new PasswordHasherOptions
|
||||
{
|
||||
CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
@@ -11,11 +14,12 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <remarks>
|
||||
/// This will check for the ASP.NET Identity password hash flag before falling back to the legacy password hashing format ("HMACSHA256")
|
||||
/// </remarks>
|
||||
public class MemberPasswordHasher : PasswordHasher<MemberIdentityUser>
|
||||
public class MemberPasswordHasher : UmbracoPasswordHasher<MemberIdentityUser>
|
||||
{
|
||||
private readonly LegacyPasswordSecurity _legacyPasswordHasher;
|
||||
|
||||
public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher) => _legacyPasswordHasher = legacyPasswordHasher ?? throw new ArgumentNullException(nameof(legacyPasswordHasher));
|
||||
public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher, IJsonSerializer jsonSerializer)
|
||||
: base(legacyPasswordHasher, jsonSerializer)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a user's hashed password
|
||||
@@ -27,6 +31,20 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <exception cref="InvalidOperationException">Thrown when the correct hashing algorith cannot be determined</exception>
|
||||
public override PasswordVerificationResult VerifyHashedPassword(MemberIdentityUser user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
// if there's password config use the base implementation
|
||||
if (!user.PasswordConfig.IsNullOrWhiteSpace())
|
||||
{
|
||||
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
}
|
||||
|
||||
// Else we need to detect what the password is. This will be the case
|
||||
// for upgrades since no password config will exist.
|
||||
|
||||
byte[] decodedHashedPassword = null;
|
||||
bool isAspNetIdentityHash = false;
|
||||
|
||||
@@ -51,7 +69,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
throw new InvalidOperationException("unable to determine member password hashing algorith");
|
||||
}
|
||||
|
||||
var isValid = _legacyPasswordHasher.VerifyPassword(
|
||||
var isValid = LegacyPasswordSecurity.VerifyPassword(
|
||||
Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName,
|
||||
providedPassword,
|
||||
hashedPassword);
|
||||
|
||||
@@ -224,12 +224,17 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <returns></returns>
|
||||
private UmbracoIdentityRole MapFromMemberGroup(IMemberGroup memberGroup)
|
||||
{
|
||||
// NOTE: there is a ConcurrencyStamp property but we don't use it. The purpose
|
||||
// of this value is to try to prevent concurrent writes in the DB but this is
|
||||
// an implementation detail at the data source level that has leaked into the
|
||||
// model. A good writeup of that is here:
|
||||
// https://stackoverflow.com/a/37362173
|
||||
// For our purposes currently we won't worry about this.
|
||||
|
||||
var result = new UmbracoIdentityRole
|
||||
{
|
||||
Id = memberGroup.Id.ToString(),
|
||||
Name = memberGroup.Name
|
||||
// TODO: Implement this functionality, requires DB and logic updates
|
||||
//ConcurrencyStamp
|
||||
};
|
||||
return result;
|
||||
}
|
||||
@@ -247,8 +252,6 @@ namespace Umbraco.Cms.Core.Security
|
||||
if (role.IsPropertyDirty(nameof(UmbracoIdentityRole.Name))
|
||||
&& !string.IsNullOrEmpty(role.Name) && memberGroup.Name != role.Name)
|
||||
{
|
||||
// TODO: Need to support ConcurrencyStamp and logic
|
||||
|
||||
memberGroup.Name = role.Name;
|
||||
anythingChanged = true;
|
||||
}
|
||||
|
||||
@@ -611,6 +611,10 @@ namespace Umbraco.Cms.Core.Security
|
||||
|
||||
public IPublishedContent GetPublishedMember(MemberIdentityUser user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
IMember member = _memberService.GetByKey(user.Key);
|
||||
if (member == null)
|
||||
{
|
||||
|
||||
@@ -57,7 +57,12 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// </summary>
|
||||
public bool HasIdentity { get; protected set; }
|
||||
|
||||
// TODO: We should support this and it's logic
|
||||
// NOTE: The purpose
|
||||
// of this value is to try to prevent concurrent writes in the DB but this is
|
||||
// an implementation detail at the data source level that has leaked into the
|
||||
// model. A good writeup of that is here:
|
||||
// https://stackoverflow.com/a/37362173
|
||||
// For our purposes currently we won't worry about this.
|
||||
public override string ConcurrencyStamp { get => base.ConcurrencyStamp; set => base.ConcurrencyStamp = value; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -69,6 +69,14 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: The purpose
|
||||
// of this value is to try to prevent concurrent writes in the DB but this is
|
||||
// an implementation detail at the data source level that has leaked into the
|
||||
// model. A good writeup of that is here:
|
||||
// https://stackoverflow.com/a/37362173
|
||||
// For our purposes currently we won't worry about this.
|
||||
public override string ConcurrencyStamp { get => base.ConcurrencyStamp; set => base.ConcurrencyStamp = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets last login date
|
||||
/// </summary>
|
||||
|
||||
92
src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs
Normal file
92
src/Umbraco.Infrastructure/Security/UmbracoPasswordHasher.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
public class UmbracoPasswordHasher<TUser> : PasswordHasher<TUser>
|
||||
where TUser: UmbracoIdentityUser
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly PasswordHasher<TUser> _aspnetV2PasswordHasher = new PasswordHasher<TUser>(new V2PasswordHasherOptions());
|
||||
|
||||
public UmbracoPasswordHasher(LegacyPasswordSecurity legacyPasswordSecurity, IJsonSerializer jsonSerializer)
|
||||
{
|
||||
LegacyPasswordSecurity = legacyPasswordSecurity ?? throw new System.ArgumentNullException(nameof(legacyPasswordSecurity));
|
||||
_jsonSerializer = jsonSerializer ?? throw new System.ArgumentNullException(nameof(jsonSerializer));
|
||||
}
|
||||
|
||||
public LegacyPasswordSecurity LegacyPasswordSecurity { get; }
|
||||
|
||||
public override string HashPassword(TUser user, string password)
|
||||
{
|
||||
// Always use the latest/current hash algorithm when hashing new passwords for storage.
|
||||
// NOTE: This is only overridden to show that we can since we may need to adjust this in the future
|
||||
// if new/different formats are required.
|
||||
return base.HashPassword(user, password);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies a user's hashed password
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="hashedPassword"></param>
|
||||
/// <param name="providedPassword"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This will check the user's current hashed password format stored with their user row and use that to verify the hash. This could be any hashes
|
||||
/// from the very old v4, to the older v6-v8, to the older aspnet identity and finally to the most recent
|
||||
/// </remarks>
|
||||
public override PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword)
|
||||
{
|
||||
if (user is null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(user));
|
||||
}
|
||||
|
||||
if (!user.PasswordConfig.IsNullOrWhiteSpace())
|
||||
{
|
||||
// check if the (legacy) password security supports this hash algorith and if so then use it
|
||||
var deserialized = _jsonSerializer.Deserialize<PersistedPasswordSettings>(user.PasswordConfig);
|
||||
if (LegacyPasswordSecurity.SupportHashAlgorithm(deserialized.HashAlgorithm))
|
||||
{
|
||||
var result = LegacyPasswordSecurity.VerifyPassword(deserialized.HashAlgorithm, providedPassword, hashedPassword);
|
||||
return result
|
||||
? PasswordVerificationResult.SuccessRehashNeeded
|
||||
: PasswordVerificationResult.Failed;
|
||||
}
|
||||
|
||||
// We will explicitly detect names here
|
||||
// The default is PBKDF2.ASPNETCORE.V3:
|
||||
// PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
|
||||
// The underlying class only lets us change 2 things which is the version: options.CompatibilityMode and the iteration count
|
||||
// The PBKDF2.ASPNETCORE.V2 settings are:
|
||||
// PBKDF2 with HMAC-SHA1, 128-bit salt, 256-bit subkey, 1000 iterations.
|
||||
|
||||
switch (deserialized.HashAlgorithm)
|
||||
{
|
||||
case Constants.Security.AspNetCoreV3PasswordHashAlgorithmName:
|
||||
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
case Constants.Security.AspNetCoreV2PasswordHashAlgorithmName:
|
||||
var legacyResult = _aspnetV2PasswordHasher.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
if (legacyResult == PasswordVerificationResult.Success)
|
||||
return PasswordVerificationResult.SuccessRehashNeeded;
|
||||
return legacyResult;
|
||||
}
|
||||
}
|
||||
|
||||
// else go the default (v3)
|
||||
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
|
||||
}
|
||||
|
||||
private class V2PasswordHasherOptions : IOptions<PasswordHasherOptions>
|
||||
{
|
||||
public PasswordHasherOptions Value => new PasswordHasherOptions
|
||||
{
|
||||
CompatibilityMode = PasswordHasherCompatibilityMode.IdentityV2
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user