diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
index 2bb9b1ab8d..9c6aeca80e 100644
--- a/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
+++ b/src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs
@@ -23,19 +23,28 @@ namespace Umbraco.Cms.Core.Security
{
}
- ///
- ///
- /// Returns a ClaimsIdentity that has the required claims, and allows flowing of claims from external identity
- ///
- public override async Task CreateAsync(BackOfficeIdentityUser user)
- {
- if (user == null)
- {
- throw new ArgumentNullException(nameof(user));
- }
+ protected virtual string AuthenticationType { get; } = Constants.Security.BackOfficeAuthenticationType;
+ ///
+ protected override async Task 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);
- }
-
- ///
- protected override async Task 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;
}
}
}
diff --git a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
index 1a37376070..d4b61a934d 100644
--- a/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
+++ b/src/Umbraco.Infrastructure/Security/ClaimsIdentityExtensions.cs
@@ -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
diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
index c0b9a19ef1..7e36081e73 100644
--- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
+++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Security
///
/// A custom user store that uses Umbraco member data
///
- public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>
+ public class MemberUserStore : UserStoreBase, IdentityUserRole, IdentityUserLogin, IdentityUserToken, IdentityRoleClaim>
{
private const string genericIdentityErrorCode = "IdentityErrorUserStore";
private readonly IMemberService _memberService;
@@ -562,7 +562,7 @@ namespace Umbraco.Cms.Core.Security
}
///
- protected override Task FindRoleAsync(string roleName, CancellationToken cancellationToken)
+ protected override Task 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()
diff --git a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs
index 9d06dcd037..00c4038287 100644
--- a/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs
+++ b/src/Umbraco.Infrastructure/Security/UmbracoIdentityRole.cs
@@ -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
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
index 86f96c7d71..bdcc5ec75b 100644
--- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Security/MemberSignInManagerTests.cs
@@ -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> _mockLogger;
- private readonly Mock> _memberManager = MockUserManager();
- private readonly Mock _mockIpResolver = new Mock();
+ private Mock>> _mockLogger;
+ private readonly Mock> _memberManager = MockUserManager();
+
+ public MemberClaimsPrincipalFactory CreateClaimsFactory(UserManager userMgr)
+ => new MemberClaimsPrincipalFactory(userMgr, Options.Create(new MemberIdentityOptions()));
public MemberSignInManager CreateSut()
{
- _mockLogger = new Mock>();
+ // 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(new HttpConnectionFeature
+ {
+ LocalIpAddress = IPAddress.Parse("127.0.0.1")
+ });
+ HttpContext httpContext = httpContextFactory.Create(features);
+
+ _mockLogger = new Mock>>();
return new MemberSignInManager(
_memberManager.Object,
- Mock.Of(),
- Mock.Of>(),
+ Mock.Of(x => x.HttpContext == httpContext),
+ CreateClaimsFactory(_memberManager.Object),
Mock.Of>(),
- Mock.Of>>(),
+ _mockLogger.Object,
Mock.Of(),
Mock.Of>());
}
- private static Mock> MockUserManager() where TUser : class
+ private static Mock> MockUserManager()
{
- var store = new Mock>();
- var mgr = new Mock>(store.Object, null, null, null, null, null, null, null, null);
+ var store = new Mock>();
+ var mgr = new Mock>(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())).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())).ReturnsAsync(userId);
+ _memberManager.Setup(x => x.GetUserNameAsync(It.IsAny())).ReturnsAsync(fakeUser.UserName);
_memberManager.Setup(x => x.FindByNameAsync(It.IsAny())).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));
}
}
}
diff --git a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
index 5182db4e20..b970f3e551 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/ServiceCollectionExtensions.cs
@@ -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()
+ .AddClaimsPrincipalFactory()
.AddUserStore()
.AddRoleStore()
- .AddRoleValidator>()
- .AddRoleManager>();
+ .AddRoleValidator>()
+ .AddRoleManager>();
private static MemberIdentityBuilder BuildMembersIdentity(this IServiceCollection services)
{
@@ -78,7 +80,7 @@ namespace Umbraco.Extensions
services.TryAddScoped, UserValidator>();
services.TryAddScoped, PasswordValidator>();
services.TryAddScoped, PasswordHasher>();
- return new MemberIdentityBuilder(typeof(IdentityRole), services);
+ return new MemberIdentityBuilder(typeof(UmbracoIdentityRole), services);
}
private static void RemoveIntParamenterIfValueGreatherThen(IDictionary commands, string parameter, int maxValue)
diff --git a/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs
new file mode 100644
index 0000000000..fe9a0eadd4
--- /dev/null
+++ b/src/Umbraco.Web.Common/Security/MemberClaimsPrincipalFactory.cs
@@ -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
+ {
+ public MemberClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor)
+ : base(userManager, optionsAccessor)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs
index 195312a41e..f3b80ba4bc 100644
--- a/src/Umbraco.Web.Common/Security/MemberManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberManager.cs
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Security;
namespace Umbraco.Cms.Web.Common.Security
{
+
public class MemberManager : UmbracoUserManager, IMemberManager
{
public MemberManager(
diff --git a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
index 392d20ebbe..eeec3c2899 100644
--- a/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
+++ b/src/Umbraco.Web.Common/Security/MemberSignInManager.cs
@@ -38,27 +38,6 @@ namespace Umbraco.Cms.Web.Common.Security
// use default scheme for members
protected override string TwoFactorRememberMeAuthenticationType => IdentityConstants.TwoFactorRememberMeScheme;
- ///
- public override async Task 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);
- }
-
- ///
- public override async Task 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);
- }
-
///
public override Task GetTwoFactorAuthenticationUserAsync()
=> throw new NotImplementedException("Two factor is not yet implemented for members");
diff --git a/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
new file mode 100644
index 0000000000..ea29098bef
--- /dev/null
+++ b/src/Umbraco.Web.Common/Security/UmbracoSignInManager.cs
@@ -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
+{
+ ///
+ /// Abstract sign in manager implementation allowing modifying all defeault authentication schemes
+ ///
+ ///
+ public abstract class UmbracoSignInManager : SignInManager
+ 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 userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation 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; }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ public override async Task 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
+ };
+ }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ 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 claims = Array.Empty();
+
+ var authenticationMethod = auth?.Principal?.FindFirst(ClaimTypes.AuthenticationMethod);
+ var amr = auth?.Principal?.FindFirst("amr");
+
+ if (authenticationMethod != null || amr != null)
+ {
+ claims = new List();
+ if (authenticationMethod != null)
+ {
+ claims.Add(authenticationMethod);
+ }
+ if (amr != null)
+ {
+ claims.Add(amr);
+ }
+ }
+
+ await SignInWithClaimsAsync(user, auth?.Properties, claims);
+ }
+
+ ///
+ public override async Task SignInWithClaimsAsync(TUser user, AuthenticationProperties authenticationProperties, IEnumerable 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());
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public override async Task 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);
+ }
+
+ ///
+ 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 });
+ }
+
+ ///
+ public override async Task 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;
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ /// Called on any login attempt to update the AccessFailedCount and to raise events
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual async Task 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;
+ }
+
+ ///
+ protected override async Task 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 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 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 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
+ {
+ 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; }
+ }
+ }
+}