Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Member password roll forward #10138

Merged
merged 38 commits into from
Apr 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e2b5c28
Getting new netcore PublicAccessChecker in place
Shazwazza Apr 12, 2021
8ec0d3f
Adds full test coverage for PublicAccessChecker
Shazwazza Apr 13, 2021
a5daad6
Merge branch 'netcore/task/member-macros-10577' into netcore/task/pub…
Shazwazza Apr 13, 2021
ad755ab
Merge remote-tracking branch 'origin/netcore/dev' into netcore/task/p…
Shazwazza Apr 13, 2021
3a31c73
remove PublicAccessComposer
Shazwazza Apr 13, 2021
41a10dc
adjust namespaces, ensure RoleManager works, separate public access c…
Shazwazza Apr 14, 2021
0f4a1c6
Merge remote-tracking branch 'origin/netcore/dev' into netcore/task/p…
Shazwazza Apr 14, 2021
d4382be
Implements the required methods on IMemberManager, removes old migrat…
Shazwazza Apr 14, 2021
2907ac7
Updates routing to be able to re-route, Fixes middleware ordering ens…
Shazwazza Apr 15, 2021
07e1ffe
adds note
Shazwazza Apr 15, 2021
e0abd35
adds note
Shazwazza Apr 15, 2021
abdb199
Cleans up ext methods, ensures that members identity is added on both…
Shazwazza Apr 15, 2021
3dfd4d7
Changes name to IUmbracoEndpointBuilder
Shazwazza Apr 15, 2021
cf770a5
adds note
Shazwazza Apr 15, 2021
cfcbe36
Fixing tests, fixing error describers so there's 2x one for back offi…
Shazwazza Apr 15, 2021
583a15c
fixing build
Shazwazza Apr 15, 2021
58d80db
Updates user manager to correctly validate password hashing and injec…
Shazwazza Apr 16, 2021
8b8bf47
Merges PR
Shazwazza Apr 16, 2021
dc2e1d6
Fixes up build and notes
Shazwazza Apr 16, 2021
a4007eb
Fixes keepalive, fixes PublicAccessMiddleware to not throw, updates s…
Shazwazza Apr 19, 2021
eba8616
adds note
Shazwazza Apr 19, 2021
70b1513
removes unused filter, fixes build
Shazwazza Apr 19, 2021
fe7975d
fixes WebPath and tests
Shazwazza Apr 19, 2021
fd86c98
Looks up entities in one query
Shazwazza Apr 19, 2021
5678256
remove usings
Shazwazza Apr 19, 2021
bafa2ac
Fix test, remove stylesheet
Shazwazza Apr 19, 2021
821a699
Set status code before we write to response to avoid error
bergmania Apr 19, 2021
f98a99c
Ensures that users and members are validated when logging in. Shares …
Shazwazza Apr 19, 2021
ec3acc4
Merge branch 'netcore/task/public-access-10578' of https://github.com…
Shazwazza Apr 19, 2021
73600dc
Fixes RepositoryCacheKeys to ensure the keys are normalized
Shazwazza Apr 20, 2021
774e05a
oops didn't mean to commit this
Shazwazza Apr 20, 2021
8326b04
Fix casing issues with caching, stop boxing value types for all cache…
Shazwazza Apr 20, 2021
cae7c1c
Merge branch 'netcore/task/public-access-10578' into netcore/task/pas…
Shazwazza Apr 20, 2021
f03b0af
oops didn't mean to comit this
Shazwazza Apr 20, 2021
afb8022
bah, far out this keeps getting recommitted. sorry
Shazwazza Apr 20, 2021
25a5fb9
Merge branch 'netcore/task/public-access-10578' into netcore/task/pas…
Shazwazza Apr 20, 2021
f0db498
Merge remote-tracking branch 'origin/netcore/dev' into netcore/task/p…
bergmania Apr 20, 2021
2b35a72
Merge remote-tracking branch 'origin/netcore/dev' into netcore/task/p…
bergmania Apr 20, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Umbraco.Infrastructure/Security/BackOfficeIdentityBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@ private void InitializeServices(IServiceCollection services)
{
// We need to manually register some identity services here because we cannot rely on normal
// AddIdentity calls for back office users
// For example: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs#L33
// For example: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs#L33
// The reason we need our own is because the Identity system doesn't cater easily for multiple identity systems and particularly being
// 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.

// Services used by identity
services.TryAddScoped<IUserValidator<BackOfficeIdentityUser>, UserValidator<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordValidator<BackOfficeIdentityUser>, PasswordValidator<BackOfficeIdentityUser>>();
services.TryAddScoped<IPasswordHasher<BackOfficeIdentityUser>>(
services.AddScoped<IUserValidator<BackOfficeIdentityUser>, UserValidator<BackOfficeIdentityUser>>();
services.AddScoped<IPasswordValidator<BackOfficeIdentityUser>, PasswordValidator<BackOfficeIdentityUser>>();
services.AddScoped<IPasswordHasher<BackOfficeIdentityUser>>(
services => new BackOfficePasswordHasher(
new LegacyPasswordSecurity(),
services.GetRequiredService<IJsonSerializer>()));
services.TryAddScoped<IUserConfirmation<BackOfficeIdentityUser>, UmbracoUserConfirmation<BackOfficeIdentityUser>>();
services.AddScoped<IUserConfirmation<BackOfficeIdentityUser>, UmbracoUserConfirmation<BackOfficeIdentityUser>>();
}

// override to add itself, by default identity only wants a single IdentityErrorDescriber
Expand Down
62 changes: 62 additions & 0 deletions src/Umbraco.Infrastructure/Security/MemberPasswordHasher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System;
using Microsoft.AspNetCore.Identity;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Security;

namespace Umbraco.Cms.Core.Security
{
/// <summary>
/// A password hasher for members
/// </summary>
/// <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>
{
private readonly LegacyPasswordSecurity _legacyPasswordHasher;

public MemberPasswordHasher(LegacyPasswordSecurity legacyPasswordHasher) => _legacyPasswordHasher = legacyPasswordHasher ?? throw new ArgumentNullException(nameof(legacyPasswordHasher));

/// <summary>
/// Verifies a user's hashed password
/// </summary>
/// <param name="user"></param>
/// <param name="hashedPassword"></param>
/// <param name="providedPassword"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown when the correct hashing algorith cannot be determined</exception>
public override PasswordVerificationResult VerifyHashedPassword(MemberIdentityUser user, string hashedPassword, string providedPassword)
{
byte[] decodedHashedPassword = null;
bool isAspNetIdentityHash = false;

try
{
decodedHashedPassword = Convert.FromBase64String(hashedPassword);
isAspNetIdentityHash = true;
}
catch (Exception)
{
// ignored - decoding throws
}

// check for default ASP.NET Identity password hash flags
if (isAspNetIdentityHash)
{
if (decodedHashedPassword[0] == 0x00 || decodedHashedPassword[0] == 0x01)
{
return base.VerifyHashedPassword(user, hashedPassword, providedPassword);
}

throw new InvalidOperationException("unable to determine member password hashing algorith");
}

var isValid = _legacyPasswordHasher.VerifyPassword(
Constants.Security.AspNetUmbraco8PasswordHashAlgorithmName,
providedPassword,
hashedPassword);

return isValid ? PasswordVerificationResult.SuccessRehashNeeded : PasswordVerificationResult.Failed;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using Microsoft.AspNetCore.Identity;
using NUnit.Framework;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Infrastructure.Security;

namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security
{
[TestFixture]
public class MemberPasswordHasherTests
{
private MemberPasswordHasher CreateSut() => new MemberPasswordHasher(new LegacyPasswordSecurity());

[Test]
public void VerifyHashedPassword_GivenAnAspNetIdentity2PasswordHash_ThenExpectSuccessRehashNeeded()
{
const string password = "Password123!";
const string hash = "AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==";

var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);

Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}

[Test]
public void VerifyHashedPassword_GivenAnAspNetCoreIdentityPasswordHash_ThenExpectSuccess()
{
const string password = "Password123!";
const string hash = "AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==";

var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);

Assert.AreEqual(result, PasswordVerificationResult.Success);
}

[Test]
public void VerifyHashedPassword_GivenALegacyPasswordHash_ThenExpectSuccessRehashNeeded()
{
const string password = "Password123!";
const string hash = "yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=";

var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, password);

Assert.AreEqual(result, PasswordVerificationResult.SuccessRehashNeeded);
}

[Test]
public void VerifyHashedPassword_GivenAnUnknownBase64Hash_ThenExpectInvalidOperationException()
{
var hashBytes = new byte[] {3, 2, 1};
var hash = Convert.ToBase64String(hashBytes);

var sut = CreateSut();
Assert.Throws<InvalidOperationException>(() => sut.VerifyHashedPassword(null, hash, "password"));
}

[TestCase("AJszAsQqxOYbASKfL3JVUu6cjU18ouizXDfX4j7wLlir8SWj2yQaTepE9e5bIohIsQ==")]
[TestCase("AQAAAAEAACcQAAAAEGF/tTVoL6ef3bQPZFYfbgKFu1CDQIAMgyY1N4EDt9jqdG/hsOX93X1U6LNvlIQ3mw==")]
[TestCase("yDiU2YyuYZU4jz6F0fpErQ==BxNRHkXBVyJs9gwWF6ktWdfDwYf5bwm+rvV7tOcNNx8=")]
public void VerifyHashedPassword_GivenAnInvalidPassword_ThenExpectFailure(string hash)
{
const string invalidPassword = "nope";

var sut = CreateSut();
var result = sut.VerifyHashedPassword(null, hash, invalidPassword);

Assert.AreEqual(result, PasswordVerificationResult.Failed);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public static IUmbracoBuilder AddBackOfficeIdentity(this IUmbracoBuilder builder
.AddClaimsPrincipalFactory<BackOfficeClaimsPrincipalFactory>()
.AddErrorDescriber<BackOfficeErrorDescriber>();

services.TryAddSingleton<IBackOfficeUserPasswordChecker, NoopBackOfficeUserPasswordChecker>();

// Configure the options specifically for the UmbracoBackOfficeIdentityOptions instance
services.ConfigureOptions<ConfigureBackOfficeIdentityOptions>();
services.ConfigureOptions<ConfigureBackOfficeSecurityStampValidatorOptions>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder)
return builder;
}

// NOTE: We are using AddIdentity which is going to add all of the default AuthN/AuthZ configurations = OK!
// This will also add all of the default identity services for our user/role types that we aren't overriding = OK!
// If a developer wishes to use Umbraco Members with different AuthN/AuthZ values, like different cookie values
// or authentication scheme's then they can call the default identity configuration methods like ConfigureApplicationCookie.
// BUT ... if a developer wishes to use the default auth schemes for entirely separate purposes alongside Umbraco members,
// then we'll probably have to change this and make it more flexible like how we do for Users. Which means booting up
// identity here with the basics and registering all of our own custom services.
// Since we are using the defaults in v8 (and below) for members, I think using the default for members now is OK!
// TODO: We may need to use services.AddIdentityCore instead if this is doing too much

services.AddIdentity<MemberIdentityUser, UmbracoIdentityRole>()
Expand All @@ -39,6 +47,8 @@ public static IUmbracoBuilder AddMembersIdentity(this IUmbracoBuilder builder)

services.ConfigureOptions<ConfigureMemberIdentityOptions>();

services.AddScoped<IPasswordHasher<MemberIdentityUser>, MemberPasswordHasher>();

services.ConfigureApplicationCookie(x =>
{
// TODO: We may want/need to configure these further
Expand Down
65 changes: 28 additions & 37 deletions src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class BackOfficeUserManager : UmbracoUserManager<BackOfficeIdentityUser,
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IEventAggregator _eventAggregator;
private readonly IBackOfficeUserPasswordChecker _backOfficeUserPasswordChecker;

public BackOfficeUserManager(
IIpResolver ipResolver,
Expand All @@ -33,53 +34,43 @@ public BackOfficeUserManager(
IHttpContextAccessor httpContextAccessor,
ILogger<UserManager<BackOfficeIdentityUser>> logger,
IOptions<UserPasswordConfigurationSettings> passwordConfiguration,
IEventAggregator eventAggregator)
IEventAggregator eventAggregator,
IBackOfficeUserPasswordChecker backOfficeUserPasswordChecker)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
{
_httpContextAccessor = httpContextAccessor;
_eventAggregator = eventAggregator;
_backOfficeUserPasswordChecker = backOfficeUserPasswordChecker;
}

/// <summary>
/// Gets or sets the default back office user password checker
/// Override to allow checking the password via the <see cref="IBackOfficeUserPasswordChecker"/> if one is configured
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; } // TODO: This isn't a good way to set this, it needs to be injected

/// <inheritdoc />
/// <remarks>
/// By default this uses the standard ASP.Net Identity approach which is:
/// * Get password store
/// * Call VerifyPasswordAsync with the password store + user + password
/// * Uses the PasswordHasher.VerifyHashedPassword to compare the stored password
///
/// In some cases people want simple custom control over the username/password check, for simplicity
/// sake, developers would like the users to simply validate against an LDAP directory but the user
/// data remains stored inside of Umbraco.
/// See: http://issues.umbraco.org/issue/U4-7032 for the use cases.
///
/// We've allowed this check to be overridden with a simple callback so that developers don't actually
/// have to implement/override this class.
/// </remarks>
public override async Task<bool> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
/// <param name="store"></param>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
protected override async Task<PasswordVerificationResult> VerifyPasswordAsync(
IUserPasswordStore<BackOfficeIdentityUser> store,
BackOfficeIdentityUser user,
string password)
{
if (BackOfficeUserPasswordChecker != null)
if (user.HasIdentity == false)
{
return PasswordVerificationResult.Failed;
}

BackOfficeUserPasswordCheckerResult result = await _backOfficeUserPasswordChecker.CheckPasswordAsync(user, password);

// if the result indicates to not fallback to the default, then return true if the credentials are valid
if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker)
{
BackOfficeUserPasswordCheckerResult result = await BackOfficeUserPasswordChecker.CheckPasswordAsync(user, password);

if (user.HasIdentity == false)
{
return false;
}

// if the result indicates to not fallback to the default, then return true if the credentials are valid
if (result != BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker)
{
return result == BackOfficeUserPasswordCheckerResult.ValidCredentials;
}
return result == BackOfficeUserPasswordCheckerResult.ValidCredentials
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}

// use the default behavior
return await base.CheckPasswordAsync(user, password);
return await base.VerifyPasswordAsync(store, user, password);
}

/// <summary>
Expand Down Expand Up @@ -139,7 +130,7 @@ public override async Task<IdentityResult> ChangePasswordAsync(BackOfficeIdentit

return result;
}

/// <inheritdoc/>
public override async Task<IdentityResult> SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset? lockoutEnd)
{
Expand Down Expand Up @@ -218,7 +209,7 @@ public SignOutSuccessResult NotifyLogoutSuccess(IPrincipal currentUser, string u
(currentUserId, ip) => new UserLogoutSuccessNotification(ip, userId, currentUserId)
);

return new SignOutSuccessResult {SignOutRedirectUrl = notification.SignOutRedirectUrl};
return new SignOutSuccessResult { SignOutRedirectUrl = notification.SignOutRedirectUrl };
}

public void NotifyPasswordChanged(IPrincipal currentUser, string userId) => Notify(currentUser,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Umbraco.Cms.Core.Security;

namespace Umbraco.Cms.Web.Common.Security
{
public class NoopBackOfficeUserPasswordChecker : IBackOfficeUserPasswordChecker
{
public Task<BackOfficeUserPasswordCheckerResult> CheckPasswordAsync(BackOfficeIdentityUser user, string password)
=> Task.FromResult(BackOfficeUserPasswordCheckerResult.FallbackToDefaultChecker);
}
}