From e5f3c4a1863cb73f37be501ee1d18a46a27778df Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Mon, 6 Apr 2020 17:23:04 +0100 Subject: [PATCH] Security stamp validator. Removed more references to ASP.NET Identity 2 --- build/NuSpecs/UmbracoCms.Web.nuspec | 2 +- .../Models/Identity/IdentityUser.cs | 3 +- .../Security/AppBuilderExtensions.cs | 11 ++- .../Security/BackOfficeUserManager.cs | 4 +- src/Umbraco.Web/Security/EmailService.cs | 56 ------------ .../Security/UmbracoEmailMessage.cs | 17 ---- .../Security/UmbracoSecurityStampValidator.cs | 86 +++++++++++++++++++ ...dHasher2.cs => UserAwarePasswordHasher.cs} | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 7 +- 9 files changed, 100 insertions(+), 90 deletions(-) delete mode 100644 src/Umbraco.Web/Security/EmailService.cs delete mode 100644 src/Umbraco.Web/Security/UmbracoEmailMessage.cs create mode 100644 src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs rename src/Umbraco.Web/Security/{UserAwarePasswordHasher2.cs => UserAwarePasswordHasher.cs} (91%) diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 7199f414b1..7630dad842 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -33,7 +33,7 @@ - + diff --git a/src/Umbraco.Web/Models/Identity/IdentityUser.cs b/src/Umbraco.Web/Models/Identity/IdentityUser.cs index df104fcafe..7bd077e879 100644 --- a/src/Umbraco.Web/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Web/Models/Identity/IdentityUser.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Microsoft.AspNet.Identity; using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Models.Identity @@ -13,7 +12,7 @@ namespace Umbraco.Web.Models.Identity /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want /// references to that so we will create our own here /// - public class IdentityUser : IUser + public class IdentityUser where TLogin : IIdentityUserLogin //NOTE: Making our role id a string where TRole : IdentityUserRole diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index 2c8129a521..20105cabe8 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -147,15 +147,14 @@ namespace Umbraco.Web.Security authOptions.Provider = new BackOfficeCookieAuthenticationProvider(userService, runtimeState, globalSettings, ioHelper, umbracoSettingsSection) { - // TODO: SB: SecurityStampValidator // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. - /*OnValidateIdentity = SecurityStampValidator - .OnValidateIdentity( - TimeSpan.FromMinutes(30), - (manager, user) => manager.GenerateUserIdentityAsync(user), - identity => identity.GetUserId()),*/ + OnValidateIdentity = UmbracoSecurityStampValidator + .OnValidateIdentity( + TimeSpan.FromMinutes(3), + (signInManager, manager, user) => signInManager.CreateUserIdentityAsync(user), + identity => identity.GetUserId()), }; diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager.cs b/src/Umbraco.Web/Security/BackOfficeUserManager.cs index b9d72ce0c1..ab69349afc 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.Security options.Password.RequireDigit = passwordConfiguration.RequireDigit; options.Password.RequireLowercase = passwordConfiguration.RequireLowercase; options.Password.RequireUppercase = passwordConfiguration.RequireUppercase; - + // Ensure Umbraco security stamp claim type is used options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier; options.ClaimsIdentity.UserNameClaimType = ClaimTypes.Name; @@ -202,7 +202,7 @@ namespace Umbraco.Web.Security protected virtual IPasswordHasher GetDefaultPasswordHasher(IPasswordConfiguration passwordConfiguration) { //we can use the user aware password hasher (which will be the default and preferred way) - return new UserAwarePasswordHasher2(new PasswordSecurity(passwordConfiguration)); + return new UserAwarePasswordHasher(new PasswordSecurity(passwordConfiguration)); } /// diff --git a/src/Umbraco.Web/Security/EmailService.cs b/src/Umbraco.Web/Security/EmailService.cs deleted file mode 100644 index e6454544ab..0000000000 --- a/src/Umbraco.Web/Security/EmailService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.ComponentModel; -using System.Net.Mail; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Umbraco.Core.Configuration; - -namespace Umbraco.Core.Security -{ - /// - /// The implementation for Umbraco - /// - public class EmailService : IIdentityMessageService - { - private readonly string _notificationEmailAddress; - private readonly IEmailSender _defaultEmailSender; - - public EmailService(string notificationEmailAddress, IEmailSender defaultEmailSender) - { - _notificationEmailAddress = notificationEmailAddress; - _defaultEmailSender = defaultEmailSender; - } - - - public async Task SendAsync(IdentityMessage message) - { - var mailMessage = new MailMessage( - _notificationEmailAddress, - message.Destination, - message.Subject, - message.Body) - { - IsBodyHtml = message.Body.IsNullOrWhiteSpace() == false - && message.Body.Contains("<") && message.Body.Contains(" - /// A custom implementation for IdentityMessage that allows the customization of how an email is sent - /// - internal class UmbracoEmailMessage : IdentityMessage - { - public IEmailSender MailSender { get; private set; } - - public UmbracoEmailMessage(IEmailSender mailSender) - { - MailSender = mailSender; - } - } -} diff --git a/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs b/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs new file mode 100644 index 0000000000..f506a64fba --- /dev/null +++ b/src/Umbraco.Web/Security/UmbracoSecurityStampValidator.cs @@ -0,0 +1,86 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Owin.Security.Cookies; +using Umbraco.Core; +using Umbraco.Web.Models.Identity; + +namespace Umbraco.Web.Security +{ + /// + /// Adapted from Microsoft.AspNet.Identity.Owin.SecurityStampValidator + /// + public class UmbracoSecurityStampValidator + { + public static Func OnValidateIdentity( + TimeSpan validateInterval, + Func> regenerateIdentityCallback, + Func getUserIdCallback) + where TManager : BackOfficeUserManager + where TUser : BackOfficeIdentityUser + { + if (getUserIdCallback == null) throw new ArgumentNullException(nameof(getUserIdCallback)); + + return async context => + { + var currentUtc = DateTimeOffset.UtcNow; + if (context.Options != null && context.Options.SystemClock != null) + { + currentUtc = context.Options.SystemClock.UtcNow; + } + + var issuedUtc = context.Properties.IssuedUtc; + + // Only validate if enough time has elapsed + var validate = issuedUtc == null; + if (issuedUtc != null) + { + var timeElapsed = currentUtc.Subtract(issuedUtc.Value); + validate = timeElapsed > validateInterval; + } + + if (validate) + { + var manager = context.OwinContext.Get(); + var signInManager = context.OwinContext.GetBackOfficeSignInManager(); + + var userId = getUserIdCallback(context.Identity); + + if (manager != null && userId != null) + { + var user = await manager.FindByIdAsync(userId); + var reject = true; + + // Refresh the identity if the stamp matches, otherwise reject + if (user != null && manager.SupportsUserSecurityStamp) + { + var securityStamp = context.Identity.FindFirst(Constants.Web.SecurityStampClaimType)?.Value; + if (securityStamp == await manager.GetSecurityStampAsync(user)) + { + reject = false; + // Regenerate fresh claims if possible and resign in + if (regenerateIdentityCallback != null) + { + var identity = await regenerateIdentityCallback.Invoke(signInManager, manager, user); + if (identity != null) + { + // Fix for regression where this value is not updated + // Setting it to null so that it is refreshed by the cookie middleware + context.Properties.IssuedUtc = null; + context.Properties.ExpiresUtc = null; + context.OwinContext.Authentication.SignIn(context.Properties, identity); + } + } + } + } + if (reject) + { + context.RejectIdentity(); + context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType); + } + } + } + }; + } + } +} diff --git a/src/Umbraco.Web/Security/UserAwarePasswordHasher2.cs b/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs similarity index 91% rename from src/Umbraco.Web/Security/UserAwarePasswordHasher2.cs rename to src/Umbraco.Web/Security/UserAwarePasswordHasher.cs index 9a44533427..d804ef0ae4 100644 --- a/src/Umbraco.Web/Security/UserAwarePasswordHasher2.cs +++ b/src/Umbraco.Web/Security/UserAwarePasswordHasher.cs @@ -4,12 +4,12 @@ using Umbraco.Web.Models.Identity; namespace Umbraco.Web.Security { - public class UserAwarePasswordHasher2 : IPasswordHasher + public class UserAwarePasswordHasher : IPasswordHasher where T : BackOfficeIdentityUser { private readonly PasswordSecurity _passwordSecurity; - public UserAwarePasswordHasher2(PasswordSecurity passwordSecurity) + public UserAwarePasswordHasher(PasswordSecurity passwordSecurity) { _passwordSecurity = passwordSecurity; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 703a52ccdc..9a2896a53a 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -1,4 +1,4 @@ - + @@ -205,7 +205,6 @@ - @@ -213,9 +212,9 @@ - - + +