From c8afd85bd38ef0dcc0b6aabc317ed7628ea2a677 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Thu, 2 Apr 2020 08:08:09 +0100 Subject: [PATCH] Initial run --- .../Editors/CurrentUserController.cs | 2 +- src/Umbraco.Web/OwinExtensions.cs | 33 +------- .../Security/AppBuilderExtensions.cs | 12 --- .../BackOfficeClaimsIdentityFactory.cs | 51 ------------ .../BackOfficeClaimsPrincipalFactory.cs | 41 ++++++++++ .../Security/BackOfficeSignInManager2.cs | 21 +++-- .../Security/BackOfficeUserManager2.cs | 79 +++++++------------ .../Security/NopLookupNormalizer.cs | 13 +++ src/Umbraco.Web/Umbraco.Web.csproj | 3 +- .../CheckIfUserTicketDataIsStaleAttribute.cs | 2 +- 10 files changed, 105 insertions(+), 152 deletions(-) delete mode 100644 src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs create mode 100644 src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs create mode 100644 src/Umbraco.Web/Security/NopLookupNormalizer.cs diff --git a/src/Umbraco.Web/Editors/CurrentUserController.cs b/src/Umbraco.Web/Editors/CurrentUserController.cs index 35ec09eaca..cbee8f6e2d 100644 --- a/src/Umbraco.Web/Editors/CurrentUserController.cs +++ b/src/Umbraco.Web/Editors/CurrentUserController.cs @@ -211,7 +211,7 @@ namespace Umbraco.Web.Editors if (passwordChangeResult.Success) { - var userMgr = this.TryGetOwinContext().Result.GetBackOfficeUserManager(); + var userMgr = this.TryGetOwinContext().Result.GetBackOfficeUserManager2(); //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword var result = new ModelWithNotifications(passwordChangeResult.Result.ResetPassword); diff --git a/src/Umbraco.Web/OwinExtensions.cs b/src/Umbraco.Web/OwinExtensions.cs index 66e7e5353e..c27e466a7e 100644 --- a/src/Umbraco.Web/OwinExtensions.cs +++ b/src/Umbraco.Web/OwinExtensions.cs @@ -51,18 +51,7 @@ namespace Umbraco.Web var ctx = owinContext.Get(typeof(HttpContextBase).FullName); return ctx == null ? Attempt.Fail() : Attempt.Succeed(ctx); } - - /// - /// Gets the back office sign in manager out of OWIN - /// - /// - /// - public static BackOfficeSignInManager2 GetBackOfficeSignInManager(this IOwinContext owinContext) - { - return owinContext.Get() - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager2)} from the {typeof(IOwinContext)}."); - } - + /// /// Gets the back office sign in manager out of OWIN /// @@ -74,24 +63,6 @@ namespace Umbraco.Web ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeSignInManager2)} from the {typeof(IOwinContext)}."); } - /// - /// Gets the back office user manager out of OWIN - /// - /// - /// - /// - /// This is required because to extract the user manager we need to user a custom service since owin only deals in generics and - /// developers could register their own user manager types - /// - public static BackOfficeUserManager2 GetBackOfficeUserManager(this IOwinContext owinContext) - { - var marker = owinContext.Get(BackOfficeUserManager2.OwinMarkerKey) - ?? throw new NullReferenceException($"No {typeof (IBackOfficeUserManagerMarker)}, i.e. no Umbraco back-office, has been registered with Owin."); - - return marker.GetManager(owinContext) - ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager2)} from the {typeof (IOwinContext)}."); - } - /// /// Gets the back office user manager out of OWIN /// @@ -110,8 +81,6 @@ namespace Umbraco.Web ?? throw new NullReferenceException($"Could not resolve an instance of {typeof (BackOfficeUserManager2)} from the {typeof (IOwinContext)}."); } - // TODO: SB: OWIN DI - /// /// Adapted from Microsoft.AspNet.Identity.Owin.OwinContextExtensions /// diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index e7f2d0272a..e88d2fbacb 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -2,7 +2,6 @@ using System.Threading; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.Extensions.Options; using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; @@ -16,7 +15,6 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Mapping; -using Umbraco.Core.Security; using Umbraco.Core.Services; using Umbraco.Net; using Umbraco.Web.Composing; @@ -53,11 +51,6 @@ namespace Umbraco.Web.Security mapper, passwordConfiguration, ipResolver, - new OptionsWrapper(new IdentityOptions()), - new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)), - new[] {new UserValidator(),}, - new[] {new PasswordValidator()}, - new UpperInvariantLookupNormalizer(), new IdentityErrorDescriber(), null, new NullLogger>())); @@ -88,11 +81,6 @@ namespace Umbraco.Web.Security passwordConfiguration, ipResolver, customUserStore, - new OptionsWrapper(new IdentityOptions()), - new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)), - new[] { new Microsoft.AspNetCore.Identity.UserValidator(), }, - new[] { new PasswordValidator() }, - new UpperInvariantLookupNormalizer(), new IdentityErrorDescriber(), null, new NullLogger>())); diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs deleted file mode 100644 index 487e16539b..0000000000 --- a/src/Umbraco.Web/Security/BackOfficeClaimsIdentityFactory.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Umbraco.Core; -using Umbraco.Core.Models.Identity; -using Umbraco.Core.Security; -using Umbraco.Web.Models.Identity; -using Constants = Umbraco.Core.Constants; - -namespace Umbraco.Web.Security -{ - public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory - where T: BackOfficeIdentityUser - { - public BackOfficeClaimsIdentityFactory() - { - SecurityStampClaimType = Constants.Security.SessionIdClaimType; - UserNameClaimType = ClaimTypes.Name; - } - - /// - /// Create a ClaimsIdentity from a user - /// - /// - /// - public override async Task CreateAsync(UserManager manager, T user, string authenticationType) - { - var baseIdentity = await base.CreateAsync(manager, user, authenticationType); - - var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, - user.Id, - user.UserName, - user.Name, - user.CalculatedContentStartNodeIds, - user.CalculatedMediaStartNodeIds, - user.Culture, - //NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written - Guid.NewGuid().ToString(), - user.SecurityStamp, - user.AllowedSections, - user.Roles.Select(x => x.RoleId).ToArray()); - - return umbracoIdentity; - } - } - - public class BackOfficeClaimsIdentityFactory : BackOfficeClaimsIdentityFactory - { } -} diff --git a/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs b/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs new file mode 100644 index 0000000000..2a90650d97 --- /dev/null +++ b/src/Umbraco.Web/Security/BackOfficeClaimsPrincipalFactory.cs @@ -0,0 +1,41 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; +using Umbraco.Core.Security; +using Umbraco.Web.Models.Identity; + +namespace Umbraco.Web.Security +{ + public class BackOfficeClaimsPrincipalFactory : UserClaimsPrincipalFactory + where TUser : BackOfficeIdentityUser + { + public BackOfficeClaimsPrincipalFactory(UserManager userManager, IOptions optionsAccessor) + : base(userManager, optionsAccessor) + { + } + + public override async Task CreateAsync(TUser user) + { + var claimsPrincipal = await base.CreateAsync(user); + + var umbracoIdentity = new UmbracoBackOfficeIdentity( + claimsPrincipal.Identity as ClaimsIdentity, + user.Id, + user.UserName, + user.Name, + user.CalculatedContentStartNodeIds, + user.CalculatedMediaStartNodeIds, + user.Culture, + //NOTE - there is no session id assigned here, this is just creating the identity, a session id will be generated when the cookie is written + Guid.NewGuid().ToString(), + user.SecurityStamp, + user.AllowedSections, + user.Roles.Select(x => x.RoleId).ToArray()); + + return new ClaimsPrincipal(umbracoIdentity); + } + } +} diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs index 37284cc04c..1f2d4f0bb9 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager2.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Options; using Microsoft.Owin; using Microsoft.Owin.Logging; using Microsoft.Owin.Security; @@ -16,11 +17,13 @@ namespace Umbraco.Web.Security { /// /// Custom sign in manager due to SignInManager not being .NET Standard. + /// Code ported from Umbraco's BackOfficeSignInManager. /// Can be removed once the web project moves to .NET Core. /// public class BackOfficeSignInManager2 : IDisposable { private readonly BackOfficeUserManager2 _userManager; + private readonly IUserClaimsPrincipalFactory _claimsPrincipalFactory; private readonly IAuthenticationManager _authenticationManager; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; @@ -28,27 +31,35 @@ namespace Umbraco.Web.Security public BackOfficeSignInManager2( BackOfficeUserManager2 userManager, + IUserClaimsPrincipalFactory claimsPrincipalFactory, IAuthenticationManager authenticationManager, ILogger logger, IGlobalSettings globalSettings, IOwinRequest request) { _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _claimsPrincipalFactory = claimsPrincipalFactory ?? throw new ArgumentNullException(nameof(claimsPrincipalFactory)); _authenticationManager = authenticationManager ?? throw new ArgumentNullException(nameof(authenticationManager)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _globalSettings = globalSettings ?? throw new ArgumentNullException(nameof(globalSettings)); _request = request ?? throw new ArgumentNullException(nameof(request)); } - public Task CreateUserIdentityAsync(BackOfficeIdentityUser user) + public async Task CreateUserIdentityAsync(BackOfficeIdentityUser user) { - throw new NotImplementedException(); + if (user == null) throw new ArgumentNullException(nameof(user)); + + var claimsPrincipal = await _claimsPrincipalFactory.CreateAsync(user); + return claimsPrincipal.Identity as ClaimsIdentity; } public static BackOfficeSignInManager2 Create(IOwinContext context, IGlobalSettings globalSettings, ILogger logger) { + var userManager = context.GetBackOfficeUserManager2(); + return new BackOfficeSignInManager2( - context.GetBackOfficeUserManager2(), + userManager, + new BackOfficeClaimsPrincipalFactory(userManager, new OptionsWrapper(userManager.Options)), context.Authentication, logger, globalSettings, @@ -146,7 +157,7 @@ namespace Umbraco.Web.Security if (requestContext != null) { - var backofficeUserManager = requestContext.GetBackOfficeUserManager(); + var backofficeUserManager = requestContext.GetBackOfficeUserManager2(); if (backofficeUserManager != null) backofficeUserManager.RaiseAccountLockedEvent(user.Id); } @@ -156,7 +167,7 @@ namespace Umbraco.Web.Security if (requestContext != null) { - var backofficeUserManager = requestContext.GetBackOfficeUserManager(); + var backofficeUserManager = requestContext.GetBackOfficeUserManager2(); if (backofficeUserManager != null) backofficeUserManager.RaiseInvalidLoginAttemptEvent(userName); } diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager2.cs b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs index 0adc92b77e..27bea647a2 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager2.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs @@ -23,22 +23,19 @@ namespace Umbraco.Web.Security IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, - IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) - : base(passwordConfiguration, ipResolver, store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { InitUserManager(this, passwordConfiguration); } #region Static Create methods - - // TODO: SB: Static Create methods for OWIN - + /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// @@ -50,25 +47,16 @@ namespace Umbraco.Web.Security UmbracoMapper mapper, IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) { var store = new BackOfficeUserStore2(userService, entityService, externalLoginService, globalSettings, mapper); - return new BackOfficeUserManager2( + + return Create( passwordConfiguration, ipResolver, store, - optionsAccessor, - passwordHasher, - userValidators, - passwordValidators, - keyNormalizer, errors, services, logger); @@ -81,24 +69,39 @@ namespace Umbraco.Web.Security IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IUserStore customUserStore, - IOptions optionsAccessor, - IPasswordHasher passwordHasher, - IEnumerable> userValidators, - IEnumerable> passwordValidators, - ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) { + var options = new IdentityOptions(); + + // Configure validation logic for usernames + var userValidators = new List> { new BackOfficeUserValidator2() }; + options.User.RequireUniqueEmail = true; + + // Configure validation logic for passwords + var passwordValidators = new List> { new PasswordValidator() }; + options.Password.RequiredLength = passwordConfiguration.RequiredLength; + options.Password.RequireNonAlphanumeric = passwordConfiguration.RequireNonLetterOrDigit; + options.Password.RequireDigit = passwordConfiguration.RequireDigit; + options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; + options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; + + options.Lockout.AllowedForNewUsers = true; + options.Lockout.MaxFailedAccessAttempts = passwordConfiguration.MaxFailedAccessAttemptsBeforeLockout; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + return new BackOfficeUserManager2( passwordConfiguration, ipResolver, customUserStore, - optionsAccessor, - passwordHasher, + new OptionsWrapper(options), userValidators, passwordValidators, - keyNormalizer, + new NopLookupNormalizer(), errors, services, logger); @@ -117,14 +120,13 @@ namespace Umbraco.Web.Security IIpResolver ipResolver, IUserStore store, IOptions optionsAccessor, - IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) - : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + : base(store, optionsAccessor, null, userValidators, passwordValidators, keyNormalizer, errors, services, logger) { PasswordConfiguration = passwordConfiguration ?? throw new ArgumentNullException(nameof(passwordConfiguration)); IpResolver = ipResolver ?? throw new ArgumentNullException(nameof(ipResolver)); @@ -146,7 +148,6 @@ namespace Umbraco.Web.Security public override bool SupportsUserPhoneNumber => false; #endregion - // TODO: SB: INIT /// /// Initializes the user manager with the correct options /// @@ -158,22 +159,8 @@ namespace Umbraco.Web.Security IPasswordConfiguration passwordConfig) // IDataProtectionProvider dataProtectionProvider { - // Configure validation logic for usernames - manager.UserValidators.Clear(); - manager.UserValidators.Add(new BackOfficeUserValidator2()); - manager.Options.User.RequireUniqueEmail = true; - - // Configure validation logic for passwords - manager.PasswordValidators.Clear(); - manager.PasswordValidators.Add(new PasswordValidator()); - manager.Options.Password.RequiredLength = passwordConfig.RequiredLength; - manager.Options.Password.RequireNonAlphanumeric = passwordConfig.RequireNonLetterOrDigit; - manager.Options.Password.RequireDigit = passwordConfig.RequireDigit; - manager.Options.Password.RequireLowercase = passwordConfig.RequireLowercase; - manager.Options.Password.RequireUppercase = passwordConfig.RequireUppercase; - //use a custom hasher based on our membership provider - manager.PasswordHasher = GetDefaultPasswordHasher(passwordConfig); + PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); // TODO: SB: manager.Options.Tokens using OWIN data protector /*if (dataProtectionProvider != null) @@ -184,12 +171,7 @@ namespace Umbraco.Web.Security }; }*/ - manager.Options.Lockout.AllowedForNewUsers = true; - manager.Options.Lockout.MaxFailedAccessAttempts = passwordConfig.MaxFailedAccessAttemptsBeforeLockout; - //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked - // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are - // locked out or not. - manager.Options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromDays(30); + } /// @@ -217,7 +199,6 @@ namespace Umbraco.Web.Security return new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)); } - /// /// Gets/sets the default back office user password checker /// diff --git a/src/Umbraco.Web/Security/NopLookupNormalizer.cs b/src/Umbraco.Web/Security/NopLookupNormalizer.cs new file mode 100644 index 0000000000..211cea076e --- /dev/null +++ b/src/Umbraco.Web/Security/NopLookupNormalizer.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Identity; + +namespace Umbraco.Web.Security +{ + /// + /// No-op lookup normalizer to maintain compatibility with ASP.NET Identity 2 + /// + public class NopLookupNormalizer : ILookupNormalizer + { + public string NormalizeName(string name) => name; + public string NormalizeEmail(string email) => email; + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d124a96209..6b1a25a15a 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -198,6 +198,7 @@ + @@ -208,6 +209,7 @@ + @@ -226,7 +228,6 @@ - diff --git a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs index c68b949bba..2e29b44200 100644 --- a/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/CheckIfUserTicketDataIsStaleAttribute.cs @@ -113,7 +113,7 @@ namespace Umbraco.Web.WebApi.Filters var owinCtx = actionContext.Request.TryGetOwinContext(); if (owinCtx) { - var signInManager = owinCtx.Result.GetBackOfficeSignInManager(); + var signInManager = owinCtx.Result.GetBackOfficeSignInManager2(); var backOfficeIdentityUser = Mapper.Map(user); await signInManager.SignInAsync(backOfficeIdentityUser, isPersistent: true, rememberBrowser: false);