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;