Fixes BackOfficeClaimsPrincipalFactory to use the correct auth type. Uses the correct UmbracoIdentityRole class, fixes up MemberSignInManagerTests, new MemberClaimsPrincipalFactory
This commit is contained in:
@@ -23,19 +23,28 @@ namespace Umbraco.Cms.Core.Security
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// Returns a ClaimsIdentity that has the required claims, and allows flowing of claims from external identity
|
||||
/// </remarks>
|
||||
public override async Task<ClaimsPrincipal> CreateAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(user));
|
||||
}
|
||||
protected virtual string AuthenticationType { get; } = Constants.Security.BackOfficeAuthenticationType;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
// NOTE: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79
|
||||
// since it's setting an authentication type which is not what we want.
|
||||
// so we override this method to change it.
|
||||
|
||||
// get the base
|
||||
ClaimsIdentity baseIdentity = await base.GenerateClaimsAsync(user);
|
||||
|
||||
// now create a new one with the correct authentication type
|
||||
var id = new ClaimsIdentity(
|
||||
AuthenticationType,
|
||||
Options.ClaimsIdentity.UserNameClaimType,
|
||||
Options.ClaimsIdentity.RoleClaimType);
|
||||
|
||||
// and merge all others from the base implementation
|
||||
id.MergeAllClaims(baseIdentity);
|
||||
|
||||
// ensure our required claims are there
|
||||
baseIdentity.AddRequiredClaims(
|
||||
user.Id,
|
||||
user.UserName,
|
||||
@@ -51,21 +60,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
// assigned which could be done in the OnExternalLogin callback
|
||||
baseIdentity.MergeClaimsFromBackOfficeIdentity(user);
|
||||
|
||||
return new ClaimsPrincipal(baseIdentity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(BackOfficeIdentityUser user)
|
||||
{
|
||||
// TODO: Have a look at the base implementation https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L79
|
||||
// since it's setting an authentication type that is probably not what we want.
|
||||
// also, this is the method that we should be returning our UmbracoBackOfficeIdentity from , not the method above,
|
||||
// the method above just returns a principal that wraps the identity and we dont use a custom principal,
|
||||
// see https://github.com/dotnet/aspnetcore/blob/master/src/Identity/Extensions.Core/src/UserClaimsPrincipalFactory.cs#L66
|
||||
|
||||
ClaimsIdentity identity = await base.GenerateClaimsAsync(user);
|
||||
|
||||
return identity;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ namespace Umbraco.Extensions
|
||||
// is re-issued and we don't want to merge old values of these.
|
||||
private static readonly string[] s_ignoredClaims = new[] { ClaimTypes.CookiePath, Constants.Security.SessionIdClaimType };
|
||||
|
||||
public static void MergeAllClaims(this ClaimsIdentity destination, ClaimsIdentity source)
|
||||
{
|
||||
foreach (Claim claim in source.Claims
|
||||
.Where(claim => !destination.HasClaim(claim.Type, claim.Value)))
|
||||
{
|
||||
destination.AddClaim(new Claim(claim.Type, claim.Value));
|
||||
}
|
||||
}
|
||||
|
||||
public static void MergeClaimsFromBackOfficeIdentity(this ClaimsIdentity destination, ClaimsIdentity source)
|
||||
{
|
||||
foreach (Claim claim in source.Claims
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
/// <summary>
|
||||
/// A custom user store that uses Umbraco member data
|
||||
/// </summary>
|
||||
public class MemberUserStore : UserStoreBase<MemberIdentityUser, IdentityRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
public class MemberUserStore : UserStoreBase<MemberIdentityUser, UmbracoIdentityRole, string, IdentityUserClaim<string>, IdentityUserRole<string>, IdentityUserLogin<string>, IdentityUserToken<string>, IdentityRoleClaim<string>>
|
||||
{
|
||||
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
|
||||
private readonly IMemberService _memberService;
|
||||
@@ -562,7 +562,7 @@ namespace Umbraco.Cms.Core.Security
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override Task<IdentityRole> FindRoleAsync(string roleName, CancellationToken cancellationToken)
|
||||
protected override Task<UmbracoIdentityRole> FindRoleAsync(string roleName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roleName))
|
||||
{
|
||||
@@ -572,10 +572,10 @@ namespace Umbraco.Cms.Core.Security
|
||||
IMemberGroup group = _memberService.GetAllRoles().SingleOrDefault(x => x.Name == roleName);
|
||||
if (group == null)
|
||||
{
|
||||
return Task.FromResult((IdentityRole)null);
|
||||
return Task.FromResult((UmbracoIdentityRole)null);
|
||||
}
|
||||
|
||||
return Task.FromResult(new IdentityRole(group.Name)
|
||||
return Task.FromResult(new UmbracoIdentityRole(group.Name)
|
||||
{
|
||||
//TODO: what should the alias be?
|
||||
Id = group.Id.ToString()
|
||||
|
||||
@@ -10,6 +10,14 @@ namespace Umbraco.Cms.Core.Models.Identity
|
||||
private string _id;
|
||||
private string _name;
|
||||
|
||||
public UmbracoIdentityRole(string roleName) : base(roleName)
|
||||
{
|
||||
}
|
||||
|
||||
public UmbracoIdentityRole()
|
||||
{
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
@@ -15,26 +19,45 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
[TestFixture]
|
||||
public class MemberSignInManagerTests
|
||||
{
|
||||
private Mock<ILogger<IMemberManager>> _mockLogger;
|
||||
private readonly Mock<UserManager<MemberIdentityUser>> _memberManager = MockUserManager<MemberIdentityUser>();
|
||||
private readonly Mock<IIpResolver> _mockIpResolver = new Mock<IIpResolver>();
|
||||
private Mock<ILogger<SignInManager<MemberIdentityUser>>> _mockLogger;
|
||||
private readonly Mock<UserManager<MemberIdentityUser>> _memberManager = MockUserManager();
|
||||
|
||||
public MemberClaimsPrincipalFactory CreateClaimsFactory(UserManager<MemberIdentityUser> userMgr)
|
||||
=> new MemberClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions()));
|
||||
|
||||
public MemberSignInManager CreateSut()
|
||||
{
|
||||
_mockLogger = new Mock<ILogger<IMemberManager>>();
|
||||
// This all needs to be setup because internally aspnet resolves a bunch
|
||||
// of services from the HttpContext.RequestServices.
|
||||
var serviceProviderFactory = new DefaultServiceProviderFactory();
|
||||
var serviceCollection = new ServiceCollection();
|
||||
serviceCollection
|
||||
.AddLogging()
|
||||
.AddAuthentication()
|
||||
.AddCookie(IdentityConstants.ApplicationScheme);
|
||||
IServiceProvider serviceProvider = serviceProviderFactory.CreateServiceProvider(serviceCollection);
|
||||
var httpContextFactory = new DefaultHttpContextFactory(serviceProvider);
|
||||
IFeatureCollection features = new DefaultHttpContext().Features;
|
||||
features.Set<IHttpConnectionFeature>(new HttpConnectionFeature
|
||||
{
|
||||
LocalIpAddress = IPAddress.Parse("127.0.0.1")
|
||||
});
|
||||
HttpContext httpContext = httpContextFactory.Create(features);
|
||||
|
||||
_mockLogger = new Mock<ILogger<SignInManager<MemberIdentityUser>>>();
|
||||
return new MemberSignInManager(
|
||||
_memberManager.Object,
|
||||
Mock.Of<IHttpContextAccessor>(),
|
||||
Mock.Of<IUserClaimsPrincipalFactory<MemberIdentityUser>>(),
|
||||
Mock.Of<IHttpContextAccessor>(x => x.HttpContext == httpContext),
|
||||
CreateClaimsFactory(_memberManager.Object),
|
||||
Mock.Of<IOptions<IdentityOptions>>(),
|
||||
Mock.Of<ILogger<SignInManager<MemberIdentityUser>>>(),
|
||||
_mockLogger.Object,
|
||||
Mock.Of<IAuthenticationSchemeProvider>(),
|
||||
Mock.Of<IUserConfirmation<MemberIdentityUser>>());
|
||||
}
|
||||
private static Mock<UserManager<TUser>> MockUserManager<TUser>() where TUser : class
|
||||
private static Mock<UserManager<MemberIdentityUser>> MockUserManager()
|
||||
{
|
||||
var store = new Mock<IUserStore<TUser>>();
|
||||
var mgr = new Mock<UserManager<TUser>>(store.Object, null, null, null, null, null, null, null, null);
|
||||
var store = new Mock<IUserStore<MemberIdentityUser>>();
|
||||
var mgr = new Mock<UserManager<MemberIdentityUser>>(store.Object, null, null, null, null, null, null, null, null);
|
||||
return mgr;
|
||||
}
|
||||
|
||||
@@ -42,8 +65,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
public async Task WhenPasswordSignInAsyncIsCalled_AndEverythingIsSetup_ThenASignInResultSucceededShouldBeReturnedAsync()
|
||||
{
|
||||
//arrange
|
||||
var userId = "bo8w3d32q9b98";
|
||||
_memberManager.Setup(x => x.GetUserIdAsync(It.IsAny<MemberIdentityUser>())).ReturnsAsync(userId);
|
||||
var userId = "bo8w3d32q9b98";
|
||||
MemberSignInManager sut = CreateSut();
|
||||
var fakeUser = new MemberIdentityUser(777)
|
||||
{
|
||||
@@ -52,6 +74,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
var password = "testPassword";
|
||||
var lockoutOnFailure = false;
|
||||
var isPersistent = true;
|
||||
|
||||
_memberManager.Setup(x => x.GetUserIdAsync(It.IsAny<MemberIdentityUser>())).ReturnsAsync(userId);
|
||||
_memberManager.Setup(x => x.GetUserNameAsync(It.IsAny<MemberIdentityUser>())).ReturnsAsync(fakeUser.UserName);
|
||||
_memberManager.Setup(x => x.FindByNameAsync(It.IsAny<string>())).ReturnsAsync(fakeUser);
|
||||
_memberManager.Setup(x => x.CheckPasswordAsync(fakeUser, password)).ReturnsAsync(true);
|
||||
_memberManager.Setup(x => x.IsEmailConfirmedAsync(fakeUser)).ReturnsAsync(true);
|
||||
@@ -76,14 +101,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Security
|
||||
var password = "testPassword";
|
||||
var lockoutOnFailure = false;
|
||||
var isPersistent = true;
|
||||
_mockIpResolver.Setup(x => x.GetCurrentRequestIpAddress()).Returns("127.0.0.1");
|
||||
|
||||
//act
|
||||
SignInResult actual = await sut.PasswordSignInAsync(fakeUser, password, isPersistent, lockoutOnFailure);
|
||||
|
||||
//assert
|
||||
Assert.IsFalse(actual.Succeeded);
|
||||
//_mockLogger.Verify(x => x.LogInformation("Login attempt failed for username TestUser from IP address 127.0.0.1", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Web.DependencyInjection;
|
||||
using SixLabors.ImageSharp.Web.Processors;
|
||||
using SixLabors.ImageSharp.Web.Providers;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Models.Identity;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
|
||||
@@ -67,10 +68,11 @@ namespace Umbraco.Extensions
|
||||
services.BuildMembersIdentity()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddMemberManager<IMemberManager, MemberManager>()
|
||||
.AddClaimsPrincipalFactory<MemberClaimsPrincipalFactory>()
|
||||
.AddUserStore<MemberUserStore>()
|
||||
.AddRoleStore<MemberRoleStore>()
|
||||
.AddRoleValidator<RoleValidator<IdentityRole>>()
|
||||
.AddRoleManager<RoleManager<IdentityRole>>();
|
||||
.AddRoleValidator<RoleValidator<UmbracoIdentityRole>>()
|
||||
.AddRoleManager<RoleManager<UmbracoIdentityRole>>();
|
||||
|
||||
private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services)
|
||||
{
|
||||
@@ -78,7 +80,7 @@ namespace Umbraco.Extensions
|
||||
services.TryAddScoped<IUserValidator<MemberIdentityUser>, UserValidator<MemberIdentityUser>>();
|
||||
services.TryAddScoped<IPasswordValidator<MemberIdentityUser>, PasswordValidator<MemberIdentityUser>>();
|
||||
services.TryAddScoped<IPasswordHasher<MemberIdentityUser>, PasswordHasher<MemberIdentityUser>>();
|
||||
return new MemberIdentityBuilder(typeof(IdentityRole), services);
|
||||
return new MemberIdentityBuilder(typeof(UmbracoIdentityRole), services);
|
||||
}
|
||||
|
||||
private static void RemoveIntParamenterIfValueGreatherThen(IDictionary<string, string> commands, string parameter, int maxValue)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Security
|
||||
{
|
||||
|
||||
public class MemberManager : UmbracoUserManager<MemberIdentityUser, MemberPasswordConfigurationSettings>, IMemberManager
|
||||
{
|
||||
public MemberManager(
|
||||
|
||||
@@ -38,27 +38,6 @@ namespace Umbraco.Cms.Web.Common.Security
|
||||
// use default scheme for members
|
||||
protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<SignInResult> PasswordSignInAsync(MemberIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure)
|
||||
{
|
||||
// overridden to handle logging/events
|
||||
SignInResult result = await base.PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
|
||||
return await HandleSignIn(user, user.UserName, result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
|
||||
{
|
||||
// overridden to handle logging/events
|
||||
MemberIdentityUser 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 Task<MemberIdentityUser> GetTwoFactorAuthenticationUserAsync()
|
||||
=> throw new NotImplementedException("Two factor is not yet implemented for members");
|
||||
|
||||
453
src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
Normal file
453
src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user