Merge remote-tracking branch 'origin/netcore/dev' into netcore/task/member-macros-10577

This commit is contained in:
Shannon
2021-04-07 18:24:57 +10:00
134 changed files with 3632 additions and 2064 deletions

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
public class MemberClaimsPrincipalFactory : UserClaimsPrincipalFactory<MemberIdentityUser>
{
public MemberClaimsPrincipalFactory(UserManager<MemberIdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor)
: base(userManager, optionsAccessor)
{
}
}
}

View File

@@ -10,21 +10,22 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
public class MemberManager : UmbracoUserManager<MembersIdentityUser, MemberPasswordConfigurationSettings>, IMemberManager
{
public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswordConfigurationSettings>, IMemberManager
{
public MemberManager(
IIpResolver ipResolver,
IUserStore<MembersIdentityUser> store,
IOptions<MembersIdentityOptions> optionsAccessor,
IPasswordHasher<MembersIdentityUser> passwordHasher,
IEnumerable<IUserValidator<MembersIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<MembersIdentityUser>> passwordValidators,
IUserStore<MemberIdentityUser> store,
IOptions<MemberIdentityOptions> optionsAccessor,
IPasswordHasher<MemberIdentityUser> passwordHasher,
IEnumerable<IUserValidator<MemberIdentityUser>> userValidators,
IEnumerable<IPasswordValidator<MemberIdentityUser>> passwordValidators,
BackOfficeIdentityErrorDescriber errors,
IServiceProvider services,
ILogger<UserManager<MembersIdentityUser>> logger,
ILogger<UserManager<MemberIdentityUser>> logger,
IOptions<MemberPasswordConfigurationSettings> passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors, services, logger, passwordConfiguration)
: base(ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, errors,
services, logger, passwordConfiguration)
{
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
/// <summary>
/// The sign in manager for members
/// </summary>
public class MemberSignInManager : UmbracoSignInManager<MemberIdentityUser>
{
public MemberSignInManager(
UserManager<MemberIdentityUser> memberManager,
IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory<MemberIdentityUser> claimsFactory,
IOptions<IdentityOptions> optionsAccessor,
ILogger<SignInManager<MemberIdentityUser>> logger,
IAuthenticationSchemeProvider schemes,
IUserConfirmation<MemberIdentityUser> confirmation) :
base(memberManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{ }
// use default scheme for members
protected override string AuthenticationType => IdentityConstants.ApplicationScheme;
// use default scheme for members
protected override string ExternalAuthenticationType => IdentityConstants.ExternalScheme;
// use default scheme for members
protected override string TwoFactorAuthenticationType => IdentityConstants.TwoFactorUserIdScheme;
// use default scheme for members
protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme;
/// <inheritdoc />
public override Task<MemberIdentityUser> GetTwoFactorAuthenticationUserAsync()
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient)
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task<bool> IsTwoFactorClientRememberedAsync(MemberIdentityUser user)
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task RememberTwoFactorClientAsync(MemberIdentityUser user)
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task ForgetTwoFactorClientAsync()
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
=> throw new NotImplementedException("Two factor is not yet implemented for members");
/// <inheritdoc />
public override Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
=> throw new NotImplementedException("External login is not yet implemented for members");
/// <inheritdoc />
public override AuthenticationProperties ConfigureExternalAuthenticationProperties(string provider, string redirectUrl, string userId = null)
=> throw new NotImplementedException("External login is not yet implemented for members");
/// <inheritdoc />
public override Task<IEnumerable<AuthenticationScheme>> GetExternalAuthenticationSchemesAsync()
=> throw new NotImplementedException("External login is not yet implemented for members");
}
}

View File

@@ -0,0 +1,453 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Models.Identity;
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.Common.Security
{
/// <summary>
/// Abstract sign in manager implementation allowing modifying all defeault authentication schemes
/// </summary>
/// <typeparam name="TUser"></typeparam>
public abstract class UmbracoSignInManager<TUser> : SignInManager<TUser>
where TUser : UmbracoIdentityUser
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
protected const string UmbracoSignInMgrLoginProviderKey = "LoginProvider";
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
protected const string UmbracoSignInMgrXsrfKey = "XsrfId";
public UmbracoSignInManager(UserManager<TUser> userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory<TUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor, ILogger<SignInManager<TUser>> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation<TUser> confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
{
}
protected abstract string AuthenticationType { get; }
protected abstract string ExternalAuthenticationType { get; }
protected abstract string TwoFactorAuthenticationType { get; }
protected abstract string TwoFactorRememberMeAuthenticationType { get; }
/// <inheritdoc />
public override async Task<SignInResult> PasswordSignInAsync(TUser user, string password, bool isPersistent, bool lockoutOnFailure)
{
// override to handle logging/events
var result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
return await HandleSignIn(user, user.UserName, result);
}
/// <inheritdoc />
public override async Task<ExternalLoginInfo> GetExternalLoginInfoAsync(string expectedXsrf = null)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
// to replace the auth scheme
var auth = await Context.AuthenticateAsync(ExternalAuthenticationType);
var items = auth?.Properties?.Items;
if (auth?.Principal == null || items == null || !items.ContainsKey(UmbracoSignInMgrLoginProviderKey))
{
return null;
}
if (expectedXsrf != null)
{
if (!items.ContainsKey(UmbracoSignInMgrXsrfKey))
{
return null;
}
var userId = items[UmbracoSignInMgrXsrfKey];
if (userId != expectedXsrf)
{
return null;
}
}
var providerKey = auth.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (providerKey == null || items[UmbracoSignInMgrLoginProviderKey] is not string provider)
{
return null;
}
var providerDisplayName = (await GetExternalAuthenticationSchemesAsync()).FirstOrDefault(p => p.Name == provider)?.DisplayName ?? provider;
return new ExternalLoginInfo(auth.Principal, provider, providerKey, providerDisplayName)
{
AuthenticationTokens = auth.Properties.GetTokens(),
AuthenticationProperties = auth.Properties
};
}
/// <inheritdoc />
public override async Task<TUser> GetTwoFactorAuthenticationUserAsync()
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// replaced in order to use a custom auth type
var info = await RetrieveTwoFactorInfoAsync();
if (info == null)
{
return null;
}
return await UserManager.FindByIdAsync(info.UserId);
}
/// <inheritdoc />
public override async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
// override to handle logging/events
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
return await HandleSignIn(null, userName, SignInResult.Failed);
}
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
/// <inheritdoc />
public override bool IsSignedIn(ClaimsPrincipal principal)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126
// replaced in order to use a custom auth type
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return principal?.Identities != null &&
principal.Identities.Any(i => i.AuthenticationType == AuthenticationType);
}
/// <inheritdoc />
public override async Task<SignInResult> TwoFactorSignInAsync(string provider, string code, bool isPersistent, bool rememberClient)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L552
// replaced in order to use a custom auth type and to implement logging/events
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
if (twoFactorInfo == null || twoFactorInfo.UserId == null)
{
return SignInResult.Failed;
}
var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
if (user == null)
{
return SignInResult.Failed;
}
var error = await PreSignInCheck(user);
if (error != null)
{
return error;
}
if (await UserManager.VerifyTwoFactorTokenAsync(user, provider, code))
{
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent, rememberClient);
return await HandleSignIn(user, user?.UserName, SignInResult.Success);
}
// If the token is incorrect, record the failure which also may cause the user to be locked out
await UserManager.AccessFailedAsync(user);
return await HandleSignIn(user, user?.UserName, SignInResult.Failed);
}
/// <inheritdoc />
public override async Task RefreshSignInAsync(TUser user)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L126
// replaced in order to use a custom auth type
var auth = await Context.AuthenticateAsync(AuthenticationType);
IList<Claim> claims = Array.Empty<Claim>();
var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod);
var amr = auth?.Principal?.FindFirst("amr");
if (authenticationMethod != null || amr != null)
{
claims = new List<Claim>();
if (authenticationMethod != null)
{
claims.Add(authenticationMethod);
}
if (amr != null)
{
claims.Add(amr);
}
}
await SignInWithClaimsAsync(user, auth?.Properties, claims);
}
/// <inheritdoc />
public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable<Claim> additionalClaims)
{
// override to replace IdentityConstants.ApplicationScheme with custom AuthenticationType
// code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// we also override to set the current HttpContext principal since this isn't done by default
var userPrincipal = await CreateUserPrincipalAsync(user);
foreach (var claim in additionalClaims)
{
userPrincipal.Identities.First().AddClaim(claim);
}
// FYI (just for informational purposes):
// This calls an ext method will eventually reaches `IAuthenticationService.SignInAsync`
// which then resolves the `IAuthenticationSignInHandler` for the current scheme
// by calling `IAuthenticationHandlerProvider.GetHandlerAsync(context, scheme);`
// which then calls `IAuthenticationSignInHandler.SignInAsync` = CookieAuthenticationHandler.HandleSignInAsync
// Also note, that when the CookieAuthenticationHandler sign in is successful we handle that event within our
// own ConfigureUmbracoBackOfficeCookieOptions which assigns the current HttpContext.User to the IPrincipal created
// Also note, this method gets called when performing 2FA logins
await Context.SignInAsync(
AuthenticationType,
userPrincipal,
authenticationProperties ?? new AuthenticationProperties());
}
/// <inheritdoc />
public override async Task SignOutAsync()
{
// override to replace IdentityConstants.ApplicationScheme with custom auth types
// code taken from aspnetcore: https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
await Context.SignOutAsync(AuthenticationType);
await Context.SignOutAsync(ExternalAuthenticationType);
await Context.SignOutAsync(TwoFactorAuthenticationType);
}
/// <inheritdoc />
public override async Task<bool> IsTwoFactorClientRememberedAsync(TUser user)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
// to replace the auth scheme
var userId = await UserManager.GetUserIdAsync(user);
var result = await Context.AuthenticateAsync(TwoFactorRememberMeAuthenticationType);
return (result?.Principal != null && result.Principal.FindFirstValue(ClaimTypes.Name) == userId);
}
/// <inheritdoc />
public override async Task RememberTwoFactorClientAsync(TUser user)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
// to replace the auth scheme
var principal = await StoreRememberClient(user);
await Context.SignInAsync(TwoFactorRememberMeAuthenticationType,
principal,
new AuthenticationProperties { IsPersistent = true });
}
/// <inheritdoc />
public override async Task<SignInResult> TwoFactorRecoveryCodeSignInAsync(string recoveryCode)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
// to replace the auth scheme
var twoFactorInfo = await RetrieveTwoFactorInfoAsync();
if (twoFactorInfo == null || twoFactorInfo.UserId == null)
{
return SignInResult.Failed;
}
var user = await UserManager.FindByIdAsync(twoFactorInfo.UserId);
if (user == null)
{
return SignInResult.Failed;
}
var result = await UserManager.RedeemTwoFactorRecoveryCodeAsync(user, recoveryCode);
if (result.Succeeded)
{
await DoTwoFactorSignInAsync(user, twoFactorInfo, isPersistent: false, rememberClient: false);
return SignInResult.Success;
}
// We don't protect against brute force attacks since codes are expected to be random.
return SignInResult.Failed;
}
/// <inheritdoc />
public override Task ForgetTwoFactorClientAsync()
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L422
// to replace the auth scheme
return Context.SignOutAsync(TwoFactorRememberMeAuthenticationType);
}
/// <summary>
/// Called on any login attempt to update the AccessFailedCount and to raise events
/// </summary>
/// <param name="user"></param>
/// <param name="username"></param>
/// <param name="result"></param>
/// <returns></returns>
protected virtual async Task<SignInResult> HandleSignIn(TUser user, string username, SignInResult result)
{
// TODO: Here I believe we can do all (or most) of the usermanager event raising so that it is not in the AuthenticationController
if (username.IsNullOrWhiteSpace())
{
username = "UNKNOWN"; // could happen in 2fa or something else weird
}
if (result.Succeeded)
{
//track the last login date
user.LastLoginDateUtc = DateTime.UtcNow;
if (user.AccessFailedCount > 0)
{
//we have successfully logged in, reset the AccessFailedCount
user.AccessFailedCount = 0;
}
await UserManager.UpdateAsync(user);
Logger.LogInformation("User: {UserName} logged in from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else if (result.IsLockedOut)
{
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}, the user is locked", username, Context.Connection.RemoteIpAddress);
}
else if (result.RequiresTwoFactor)
{
Logger.LogInformation("Login attempt requires verification for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else if (!result.Succeeded || result.IsNotAllowed)
{
Logger.LogInformation("Login attempt failed for username {UserName} from IP address {IpAddress}", username, Context.Connection.RemoteIpAddress);
}
else
{
throw new ArgumentOutOfRangeException();
}
return result;
}
/// <inheritdoc />
protected override async Task<SignInResult> SignInOrTwoFactorAsync(TUser user, bool isPersistent, string loginProvider = null, bool bypassTwoFactor = false)
{
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// to replace custom auth types
if (!bypassTwoFactor && await IsTfaEnabled(user))
{
if (!await IsTwoFactorClientRememberedAsync(user))
{
// Store the userId for use after two factor check
var userId = await UserManager.GetUserIdAsync(user);
await Context.SignInAsync(IdentityConstants.TwoFactorUserIdScheme, StoreTwoFactorInfo(userId, loginProvider));
return SignInResult.TwoFactorRequired;
}
}
// Cleanup external cookie
if (loginProvider != null)
{
await Context.SignOutAsync(ExternalAuthenticationType);
}
if (loginProvider == null)
{
await SignInWithClaimsAsync(user, isPersistent, new Claim[] { new Claim("amr", "pwd") });
}
else
{
await SignInAsync(user, isPersistent, loginProvider);
}
return SignInResult.Success;
}
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L782
// since it's not public
private async Task<bool> IsTfaEnabled(TUser user)
=> UserManager.SupportsUserTwoFactor &&
await UserManager.GetTwoFactorEnabledAsync(user) &&
(await UserManager.GetValidTwoFactorProvidersAsync(user)).Count > 0;
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L743
// to replace custom auth types
private ClaimsPrincipal StoreTwoFactorInfo(string userId, string loginProvider)
{
var identity = new ClaimsIdentity(TwoFactorAuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, userId));
if (loginProvider != null)
{
identity.AddClaim(new Claim(ClaimTypes.AuthenticationMethod, loginProvider));
}
return new ClaimsPrincipal(identity);
}
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// copy is required in order to use custom auth types
private async Task<ClaimsPrincipal> StoreRememberClient(TUser user)
{
var userId = await UserManager.GetUserIdAsync(user);
var rememberBrowserIdentity = new ClaimsIdentity(TwoFactorRememberMeAuthenticationType);
rememberBrowserIdentity.AddClaim(new Claim(ClaimTypes.Name, userId));
if (UserManager.SupportsUserSecurityStamp)
{
var stamp = await UserManager.GetSecurityStampAsync(user);
rememberBrowserIdentity.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType, stamp));
}
return new ClaimsPrincipal(rememberBrowserIdentity);
}
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// copy is required in order to use a custom auth type
private async Task<TwoFactorAuthenticationInfo> RetrieveTwoFactorInfoAsync()
{
var result = await Context.AuthenticateAsync(TwoFactorAuthenticationType);
if (result?.Principal != null)
{
return new TwoFactorAuthenticationInfo
{
UserId = result.Principal.FindFirstValue(ClaimTypes.Name),
LoginProvider = result.Principal.FindFirstValue(ClaimTypes.AuthenticationMethod)
};
}
return null;
}
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs
// copy is required in order to use custom auth types
private async Task DoTwoFactorSignInAsync(TUser user, TwoFactorAuthenticationInfo twoFactorInfo, bool isPersistent, bool rememberClient)
{
// When token is verified correctly, clear the access failed count used for lockout
await ResetLockout(user);
var claims = new List<Claim>
{
new Claim("amr", "mfa")
};
// Cleanup external cookie
if (twoFactorInfo.LoginProvider != null)
{
claims.Add(new Claim(ClaimTypes.AuthenticationMethod, twoFactorInfo.LoginProvider));
await Context.SignOutAsync(ExternalAuthenticationType);
}
// Cleanup two factor user id cookie
await Context.SignOutAsync(TwoFactorAuthenticationType);
if (rememberClient)
{
await RememberTwoFactorClientAsync(user);
}
await SignInWithClaimsAsync(user, isPersistent, claims);
}
// borrowed from https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Core/src/SignInManager.cs#L891
private class TwoFactorAuthenticationInfo
{
public string UserId { get; set; }
public string LoginProvider { get; set; }
}
}
}