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:
Shannon Deminick
2021-04-22 23:59:13 +10:00
committed by GitHub
parent 473bc53c66
commit 39aeec0f1f
34 changed files with 483 additions and 200 deletions

View File

@@ -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
};
}
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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)
{

View File

@@ -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>

View File

@@ -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>

View 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
};
}
}
}