From 4e583db68e3a23a7ddd6e9db9ef9e3f4d6667a74 Mon Sep 17 00:00:00 2001 From: Scott Brady Date: Thu, 2 Apr 2020 15:55:00 +0100 Subject: [PATCH] Added OWIN based token provider. Removed use of IdentityFactoryOptions --- .../Security/AppBuilderExtensions.cs | 32 +---- .../Security/BackOfficeUserManager2.cs | 40 +++--- .../OwinDataProtectorTokenProvider.cs | 130 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 4 files changed, 152 insertions(+), 51 deletions(-) create mode 100644 src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs diff --git a/src/Umbraco.Web/Security/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/AppBuilderExtensions.cs index e88d2fbacb..1222d10971 100644 --- a/src/Umbraco.Web/Security/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/AppBuilderExtensions.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Security passwordConfiguration, ipResolver, new IdentityErrorDescriber(), - null, + app.GetDataProtectionProvider(), new NullLogger>())); app.SetBackOfficeUserManagerType(); @@ -82,7 +82,7 @@ namespace Umbraco.Web.Security ipResolver, customUserStore, new IdentityErrorDescriber(), - null, + app.GetDataProtectionProvider(), new NullLogger>())); app.SetBackOfficeUserManagerType(); @@ -91,34 +91,6 @@ namespace Umbraco.Web.Security app.CreatePerOwinContext((options, context) => BackOfficeSignInManager2.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager2).FullName))); } - // TODO: SB: ConfigureUserManagerForUmbracoBackOffice using IdentityFactoryOptions - /*/// - /// Configure a custom BackOfficeUserManager for Umbraco - /// - /// - /// - /// - /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, - IRuntimeState runtimeState, - IGlobalSettings globalSettings, - Func, IOwinContext, TManager> userManager) - where TManager : BackOfficeUserManager2 - where TUser : BackOfficeIdentityUser - { - if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState)); - if (userManager == null) throw new ArgumentNullException(nameof(userManager)); - - //Configure Umbraco user manager to be created per request - app.CreatePerOwinContext(userManager); - - app.SetBackOfficeUserManagerType(); - - //Create a sign in manager per request - app.CreatePerOwinContext( - (options, context) => BackOfficeSignInManager2.Create(context, globalSettings, app.CreateLogger(typeof(BackOfficeSignInManager2).FullName))); - }*/ - /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// diff --git a/src/Umbraco.Web/Security/BackOfficeUserManager2.cs b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs index 27bea647a2..af83982647 100644 --- a/src/Umbraco.Web/Security/BackOfficeUserManager2.cs +++ b/src/Umbraco.Web/Security/BackOfficeUserManager2.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Owin.Security.DataProtection; using Umbraco.Core.Configuration; using Umbraco.Core.Mapping; using Umbraco.Core.Security; @@ -27,11 +28,11 @@ namespace Umbraco.Web.Security IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, - IServiceProvider services, + IDataProtectionProvider dataProtectionProvider, ILogger> logger) - : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + : base(passwordConfiguration, ipResolver, store, optionsAccessor, userValidators, passwordValidators, keyNormalizer, errors, null, logger) { - InitUserManager(this, passwordConfiguration); + InitUserManager(this, dataProtectionProvider); } #region Static Create methods @@ -48,7 +49,7 @@ namespace Umbraco.Web.Security IPasswordConfiguration passwordConfiguration, IIpResolver ipResolver, IdentityErrorDescriber errors, - IServiceProvider services, + IDataProtectionProvider dataProtectionProvider, ILogger> logger) { var store = new BackOfficeUserStore2(userService, entityService, externalLoginService, globalSettings, mapper); @@ -58,7 +59,7 @@ namespace Umbraco.Web.Security ipResolver, store, errors, - services, + dataProtectionProvider, logger); } @@ -70,7 +71,7 @@ namespace Umbraco.Web.Security IIpResolver ipResolver, IUserStore customUserStore, IdentityErrorDescriber errors, - IServiceProvider services, + IDataProtectionProvider dataProtectionProvider, ILogger> logger) { var options = new IdentityOptions(); @@ -103,7 +104,7 @@ namespace Umbraco.Web.Security passwordValidators, new NopLookupNormalizer(), errors, - services, + dataProtectionProvider, logger); } @@ -151,27 +152,24 @@ namespace Umbraco.Web.Security /// /// Initializes the user manager with the correct options /// - /// - /// - /// protected void InitUserManager( BackOfficeUserManager2 manager, - IPasswordConfiguration passwordConfig) - // IDataProtectionProvider dataProtectionProvider + IDataProtectionProvider dataProtectionProvider) { //use a custom hasher based on our membership provider PasswordHasher = GetDefaultPasswordHasher(PasswordConfiguration); - // TODO: SB: manager.Options.Tokens using OWIN data protector - /*if (dataProtectionProvider != null) + // TODO: SB: manager.Options.Tokens using OWIN data protector - what about the other providers??? + // https://github.com/dotnet/aspnetcore/blob/0a0e1ea0cdbe29f2fcd2291b900db98597387d77/src/Identity/Core/src/IdentityBuilderExtensions.cs#L28 + if (dataProtectionProvider != null) { - manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) - { - TokenLifespan = TimeSpan.FromDays(3) - }; - }*/ - - + manager.RegisterTokenProvider( + "Default", + new OwinDataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")) + { + TokenLifespan = TimeSpan.FromDays(3) + }); + } } /// diff --git a/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs new file mode 100644 index 0000000000..4e90980478 --- /dev/null +++ b/src/Umbraco.Web/Security/OwinDataProtectorTokenProvider.cs @@ -0,0 +1,130 @@ +using System; +using System.Globalization; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Identity; +using Microsoft.Owin.Security.DataProtection; +using Umbraco.Web.Models.Identity; + +namespace Umbraco.Web.Security +{ + /// + /// Adapted from Microsoft.AspNet.Identity.Owin.DataProtectorTokenProvider + /// + public class OwinDataProtectorTokenProvider : IUserTwoFactorTokenProvider where TUser : BackOfficeIdentityUser + { + public TimeSpan TokenLifespan { get; set; } + private readonly IDataProtector _protector; + + public OwinDataProtectorTokenProvider(IDataProtector protector) + { + _protector = protector ?? throw new ArgumentNullException(nameof(protector)); + TokenLifespan = TimeSpan.FromDays(1); + } + + public async Task GenerateAsync(string purpose, UserManager manager, TUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + var ms = new MemoryStream(); + using (var writer = ms.CreateWriter()) + { + writer.Write(DateTimeOffset.UtcNow); + writer.Write(Convert.ToString(user.Id, CultureInfo.InvariantCulture)); + writer.Write(purpose ?? ""); + + string stamp = null; + if (manager.SupportsUserSecurityStamp) + { + stamp = await manager.GetSecurityStampAsync(user); + } + writer.Write(stamp ?? ""); + } + + var protectedBytes = _protector.Protect(ms.ToArray()); + return Convert.ToBase64String(protectedBytes); + } + + public async Task ValidateAsync(string purpose, string token, UserManager manager, TUser user) + { + try + { + var unprotectedData = _protector.Unprotect(Convert.FromBase64String(token)); + var ms = new MemoryStream(unprotectedData); + using (var reader = ms.CreateReader()) + { + var creationTime = reader.ReadDateTimeOffset(); + var expirationTime = creationTime + TokenLifespan; + if (expirationTime < DateTimeOffset.UtcNow) + { + return false; + } + + var userId = reader.ReadString(); + if (!string.Equals(userId, Convert.ToString(user.Id, CultureInfo.InvariantCulture))) + { + return false; + } + + var purp = reader.ReadString(); + if (!string.Equals(purp, purpose)) + { + return false; + } + + var stamp = reader.ReadString(); + if (reader.PeekChar() != -1) + { + return false; + } + + if (manager.SupportsUserSecurityStamp) + { + var expectedStamp = await manager.GetSecurityStampAsync(user); + return stamp == expectedStamp; + } + + return stamp == ""; + } + } + // ReSharper disable once EmptyGeneralCatchClause + catch + { + // Do not leak exception + } + + return false; + } + + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, TUser user) + { + return Task.FromResult(true); + } + } + + internal static class StreamExtensions + { + private static readonly Encoding DefaultEncoding = new UTF8Encoding(false, true); + + public static BinaryReader CreateReader(this Stream stream) + { + return new BinaryReader(stream, DefaultEncoding, true); + } + + public static BinaryWriter CreateWriter(this Stream stream) + { + return new BinaryWriter(stream, DefaultEncoding, true); + } + + public static DateTimeOffset ReadDateTimeOffset(this BinaryReader reader) + { + return new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); + } + + public static void Write(this BinaryWriter writer, DateTimeOffset value) + { + writer.Write(value.UtcTicks); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 6b1a25a15a..a53187f46b 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -210,6 +210,7 @@ +