From c6481bdabb749f6bb4229222b5d421229d5e459f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 3 Jun 2020 12:47:40 +1000 Subject: [PATCH] Migrates another methods of authentication controller over along with calculating the ticket's remaining seconds --- .../BackOffice/ClaimsPrincipalExtensions.cs | 28 ++++++++++++ src/Umbraco.Core/Constants-Security.cs | 1 + .../BackOffice/BackOfficeUserManager.cs | 8 ++-- .../ClaimsPrincipalExtensionsTests.cs | 43 ++++++++++++++++++ .../Controllers/AuthenticationController.cs | 26 +++++++++++ .../ConfigureBackOfficeCookieOptions.cs | 44 +++++++++++++------ .../Security/BackOfficeSignInManager.cs | 12 +---- .../Editors/AuthenticationController.cs | 28 ------------ .../Security/AuthenticationExtensions.cs | 12 +---- 9 files changed, 136 insertions(+), 66 deletions(-) create mode 100644 src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs diff --git a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs index fa89ee6975..62b827dc6d 100644 --- a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Principal; @@ -42,5 +43,32 @@ namespace Umbraco.Extensions return null; } + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user) => user.GetRemainingAuthSeconds(DateTimeOffset.UtcNow); + + /// + /// Returns the remaining seconds on an auth ticket for the user based on the claim applied to the user durnig authentication + /// + /// + /// + /// + public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) + { + var umbIdentity = user.GetUmbracoIdentity(); + if (umbIdentity == null) return 0; + + var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType); + if (ticketExpires.IsNullOrWhiteSpace()) return 0; + + var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind); + + var secondsRemaining = utcExpired.Subtract(now).TotalSeconds; + return secondsRemaining; + } } } diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs index 534070be6e..fda4c23dc0 100644 --- a/src/Umbraco.Core/Constants-Security.cs +++ b/src/Umbraco.Core/Constants-Security.cs @@ -52,6 +52,7 @@ public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + public const string TicketExpiresClaimType = "http://umbraco.org/2020/06/identity/claims/backoffice/ticketexpires"; /// /// The claim type for the ASP.NET Identity security stamp diff --git a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs index 0ef3ff47ff..1ac7a7f1ec 100644 --- a/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs +++ b/src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs @@ -53,10 +53,11 @@ namespace Umbraco.Core.BackOffice } #region What we do not currently support - // TODO: We could support this - but a user claims will mostly just be what is in the auth cookie + + // We don't support an IUserClaimStore and don't need to (at least currently) public override bool SupportsUserClaim => false; - // TODO: Support this + // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository public override bool SupportsQueryableUsers => false; /// @@ -64,8 +65,9 @@ namespace Umbraco.Core.BackOffice /// public override bool SupportsUserTwoFactor => false; - // TODO: Support this + // We haven't needed to support this yet, though might be necessary for 2FA public override bool SupportsUserPhoneNumber => false; + #endregion /// diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs new file mode 100644 index 0000000000..a078456f8f --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/ClaimsPrincipalExtensionsTests.cs @@ -0,0 +1,43 @@ +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Umbraco.Extensions; +using Umbraco.Core; +using Umbraco.Core.BackOffice; + +namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions +{ + [TestFixture] + public class ClaimsPrincipalExtensionsTests + { + [Test] + public void Get_Remaining_Ticket_Seconds() + { + var backOfficeIdentity = new UmbracoBackOfficeIdentity(-1, "test", "test", + Enumerable.Empty(), Enumerable.Empty(), "en-US", Guid.NewGuid().ToString(), + Enumerable.Empty(), Enumerable.Empty()); + var principal = new ClaimsPrincipal(backOfficeIdentity); + + var expireSeconds = 99; + var elapsedSeconds = 3; + var remainingSeconds = expireSeconds - elapsedSeconds; + var now = DateTimeOffset.Now; + var then = now.AddSeconds(elapsedSeconds); + var expires = now.AddSeconds(expireSeconds).ToString("o"); + + backOfficeIdentity.AddClaim(new Claim( + Constants.Security.TicketExpiresClaimType, + expires, + ClaimValueTypes.DateTime, + UmbracoBackOfficeIdentity.Issuer, + UmbracoBackOfficeIdentity.Issuer, + backOfficeIdentity)); + + var ticketRemainingSeconds = principal.GetRemainingAuthSeconds(then); + + Assert.AreEqual(remainingSeconds, ticketRemainingSeconds); + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index f501fb1579..03718cb1c4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Exceptions; @@ -71,6 +72,31 @@ namespace Umbraco.Web.BackOffice.Controllers return false; } + /// + /// Returns the currently logged in Umbraco user + /// + /// + /// + /// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user + /// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session + /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. + /// + [UmbracoAuthorize] + [TypeFilter(typeof(SetAngularAntiForgeryTokens))] + //[CheckIfUserTicketDataIsStale] // TODO: Migrate this, though it will need to be done differently at the cookie auth level + public UserDetail GetCurrentUser() + { + var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + + var user = umbracoContext.Security.CurrentUser; + var result = _umbracoMapper.Map(user); + + //set their remaining seconds + result.SecondsUntilTimeout = HttpContext.User.GetRemainingAuthSeconds(); + + return result; + } + /// /// Logs a user in /// diff --git a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs index f2c3bedb48..f3db54093c 100644 --- a/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/ConfigureBackOfficeCookieOptions.cs @@ -15,7 +15,9 @@ using Umbraco.Core.Hosting; using Umbraco.Core.Services; using Umbraco.Net; using Umbraco.Core.Security; -using Umbraco.Web; +using Umbraco.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Web.Common.Security; namespace Umbraco.Web.BackOffice.Security { @@ -34,7 +36,6 @@ namespace Umbraco.Web.BackOffice.Security private readonly IUserService _userService; private readonly IIpResolver _ipResolver; private readonly BackOfficeSessionIdValidator _sessionIdValidator; - private readonly BackOfficeSecurityStampValidator _securityStampValidator; public ConfigureBackOfficeCookieOptions( IUmbracoContextAccessor umbracoContextAccessor, @@ -46,8 +47,7 @@ namespace Umbraco.Web.BackOffice.Security IRequestCache requestCache, IUserService userService, IIpResolver ipResolver, - BackOfficeSessionIdValidator sessionIdValidator, - BackOfficeSecurityStampValidator securityStampValidator) + BackOfficeSessionIdValidator sessionIdValidator) { _umbracoContextAccessor = umbracoContextAccessor; _securitySettings = securitySettings; @@ -59,7 +59,6 @@ namespace Umbraco.Web.BackOffice.Security _userService = userService; _ipResolver = ipResolver; _sessionIdValidator = sessionIdValidator; - _securityStampValidator = securityStampValidator; } public void Configure(string name, CookieAuthenticationOptions options) @@ -123,24 +122,40 @@ namespace Umbraco.Web.BackOffice.Security OnValidatePrincipal = async ctx => { - //ensure the thread culture is set - ctx.Principal?.Identity?.EnsureCulture(); + // We need to resolve the BackOfficeSecurityStampValidator per request as a requirement (even in aspnetcore they do this) + var securityStampValidator = ctx.HttpContext.RequestServices.GetRequiredService(); + // Same goes for the signinmanager + var signInManager = ctx.HttpContext.RequestServices.GetRequiredService(); - await EnsureValidSessionId(ctx); - - if (ctx.Principal?.Identity == null) + var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); + if (backOfficeIdentity == null) { - await ctx.HttpContext.SignOutAsync(Constants.Security.BackOfficeAuthenticationType); - return; + ctx.RejectPrincipal(); + await signInManager.SignOutAsync(); } - await _securityStampValidator.ValidateAsync(ctx); + //ensure the thread culture is set + backOfficeIdentity.EnsureCulture(); + + await EnsureValidSessionId(ctx); + await securityStampValidator.ValidateAsync(ctx); + + // add a claim to track when the cookie expires, we use this to track time remaining + backOfficeIdentity.AddClaim(new Claim( + Constants.Security.TicketExpiresClaimType, + ctx.Properties.ExpiresUtc.Value.ToString("o"), + ClaimValueTypes.DateTime, + UmbracoBackOfficeIdentity.Issuer, + UmbracoBackOfficeIdentity.Issuer, + backOfficeIdentity)); + }, OnSigningIn = ctx => { // occurs when sign in is successful but before the ticket is written to the outbound cookie - if (ctx.Principal.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) + var backOfficeIdentity = ctx.Principal.GetUmbracoIdentity(); + if (backOfficeIdentity != null) { //generate a session id and assign it //create a session token - if we are configured and not in an upgrade state then use the db, otherwise just generate one @@ -168,6 +183,7 @@ namespace Umbraco.Web.BackOffice.Security OnSigningOut = ctx => { //Clear the user's session on sign out + // TODO: We need to test this once we have signout functionality, not sure if the httpcontext.user.identity will still be set here if (ctx.HttpContext?.User?.Identity != null) { var claimsIdentity = ctx.HttpContext.User.Identity as ClaimsIdentity; diff --git a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs index dd0afe5a80..b39e54935d 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeSignInManager.cs @@ -10,6 +10,7 @@ using System.Security.Claims; using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.BackOffice; +using Umbraco.Extensions; namespace Umbraco.Web.Common.Security { @@ -34,17 +35,6 @@ namespace Umbraco.Web.Common.Security _userManager = userManager; } - // TODO: Implement these, this is what the security stamp thingy calls - public override Task ValidateSecurityStampAsync(BackOfficeIdentityUser user, string securityStamp) - { - return base.ValidateSecurityStampAsync(user, securityStamp); - } - // TODO: Implement these, this is what the security stamp thingy calls - public override Task ValidateSecurityStampAsync(ClaimsPrincipal principal) - { - return base.ValidateSecurityStampAsync(principal); - } - // TODO: Need to migrate more from Umbraco.Web.Security.BackOfficeSignInManager // Things like dealing with auto-linking, cookie options, and a ton of other stuff. Some might not need to be ported but it // will be a case by case basis. diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 7e27a0dcde..1cfd2f7da3 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -157,34 +157,6 @@ namespace Umbraco.Web.Editors } - /// - /// Returns the currently logged in Umbraco user - /// - /// - /// - /// We have the attribute [SetAngularAntiForgeryTokens] applied because this method is called initially to determine if the user - /// is valid before the login screen is displayed. The Auth cookie can be persisted for up to a day but the csrf cookies are only session - /// cookies which means that the auth cookie could be valid but the csrf cookies are no longer there, in that case we need to re-set the csrf cookies. - /// - [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] - [CheckIfUserTicketDataIsStale] - public UserDetail GetCurrentUser() - { - // TODO: We need to migrate this next - - var user = UmbracoContext.Security.CurrentUser; - var result = Mapper.Map(user); - var httpContextAttempt = TryGetHttpContext(); - if (httpContextAttempt.Success) - { - //set their remaining seconds - result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); - } - - return result; - } - /// /// When a user is invited they are not approved but we need to resolve the partially logged on (non approved) /// user. diff --git a/src/Umbraco.Web/Security/AuthenticationExtensions.cs b/src/Umbraco.Web/Security/AuthenticationExtensions.cs index 8e9f2abc01..2b8ba2ddfb 100644 --- a/src/Umbraco.Web/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Web/Security/AuthenticationExtensions.cs @@ -126,11 +126,7 @@ namespace Umbraco.Web.Security return true; } - /// - /// returns the number of seconds the user has until their auth session times out - /// - /// - /// + // NOTE: Migrated to netcore (though in a different way) public static double GetRemainingAuthSeconds(this HttpContextBase http) { if (http == null) throw new ArgumentNullException(nameof(http)); @@ -138,11 +134,7 @@ namespace Umbraco.Web.Security return ticket.GetRemainingAuthSeconds(); } - /// - /// returns the number of seconds the user has until their auth session times out - /// - /// - /// + // NOTE: Migrated to netcore (though in a different way) public static double GetRemainingAuthSeconds(this AuthenticationTicket ticket) { var utcExpired = ticket?.Properties.ExpiresUtc;