diff --git a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs index 7cbca0428a..6a792ce69b 100644 --- a/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs +++ b/src/Umbraco.Core/BackOffice/ClaimsPrincipalExtensions.cs @@ -17,8 +17,6 @@ namespace Umbraco.Extensions /// public static UmbracoBackOfficeIdentity GetUmbracoIdentity(this IPrincipal user) { - // TODO: It would be nice to get rid of this and only rely on Claims, not a strongly typed identity instance - //If it's already a UmbracoBackOfficeIdentity if (user.Identity is UmbracoBackOfficeIdentity backOfficeIdentity) return backOfficeIdentity; @@ -55,10 +53,10 @@ namespace Umbraco.Extensions /// public static double GetRemainingAuthSeconds(this IPrincipal user, DateTimeOffset now) { - var claimsPrincipal = user as ClaimsPrincipal; - if (claimsPrincipal == null) return 0; + var umbIdentity = user.GetUmbracoIdentity(); + if (umbIdentity == null) return 0; - var ticketExpires = claimsPrincipal.FindFirst(Constants.Security.TicketExpiresClaimType)?.Value; + var ticketExpires = umbIdentity.FindFirstValue(Constants.Security.TicketExpiresClaimType); if (ticketExpires.IsNullOrWhiteSpace()) return 0; var utcExpired = DateTimeOffset.Parse(ticketExpires, null, DateTimeStyles.RoundtripKind); diff --git a/src/Umbraco.Core/Security/IBackofficeSecurity.cs b/src/Umbraco.Core/Security/IBackofficeSecurity.cs index 12d6dc1688..4ba20f7bfa 100644 --- a/src/Umbraco.Core/Security/IBackofficeSecurity.cs +++ b/src/Umbraco.Core/Security/IBackofficeSecurity.cs @@ -32,6 +32,13 @@ namespace Umbraco.Core.Security /// ValidateRequestAttempt ValidateCurrentUser(bool throwExceptions, bool requiresApproval = true); + /// + /// Authorizes the full request, checks for SSL and validates the current user + /// + /// set to true if you want exceptions to be thrown if failed + /// + ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false); + /// /// Checks if the specified user as access to the app /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index e9f9c9fa69..bf1271c910 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net; -using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; @@ -207,26 +206,18 @@ namespace Umbraco.Web.BackOffice.Controllers } [HttpGet] - public async Task GetRemainingTimeoutSeconds() + public double GetRemainingTimeoutSeconds() { - // force authentication to occur since this is not an authorized endpoint - var result = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - if (!result.Succeeded) - { - return 0; - } - + var backOfficeIdentity = HttpContext.User.GetUmbracoIdentity(); var remainingSeconds = HttpContext.User.GetRemainingAuthSeconds(); - if (remainingSeconds <= 30) + if (remainingSeconds <= 30 && backOfficeIdentity != null) { - var username = result.Principal.FindFirst(ClaimTypes.Name)?.Value; - //NOTE: We are using 30 seconds because that is what is coded into angular to force logout to give some headway in // the timeout process. _logger.LogInformation( "User logged will be logged out due to timeout: {Username}, IP Address: {IPAddress}", - username ?? "unknown", + backOfficeIdentity.Name, _ipResolver.GetCurrentRequestIpAddress()); } @@ -238,11 +229,14 @@ namespace Umbraco.Web.BackOffice.Controllers /// /// [HttpGet] - public async Task IsAuthenticated() + public bool IsAuthenticated() { - // force authentication to occur since this is not an authorized endpoint - var result = await HttpContext.AuthenticateAsync(Constants.Security.BackOfficeAuthenticationType); - return result.Succeeded; + var attempt = _backofficeSecurityAccessor.BackOfficeSecurity.AuthorizeRequest(); + if (attempt == ValidateRequestAttempt.Success) + { + return true; + } + return false; } /// @@ -592,6 +586,7 @@ namespace Umbraco.Web.BackOffice.Controllers } + /// /// Return the for the given /// diff --git a/src/Umbraco.Web.Common/ApplicationModels/AuthenticateAsBackOfficeSchemeConvention.cs b/src/Umbraco.Web.Common/ApplicationModels/AuthenticateAsBackOfficeSchemeConvention.cs new file mode 100644 index 0000000000..838cc59ac4 --- /dev/null +++ b/src/Umbraco.Web.Common/ApplicationModels/AuthenticateAsBackOfficeSchemeConvention.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc.ApplicationModels; +using Umbraco.Web.Common.Filters; + +namespace Umbraco.Web.Common.ApplicationModels +{ + /// + /// Ensures all requests with this convention are authenticated with the back office scheme + /// + public class AuthenticateAsBackOfficeSchemeConvention : IActionModelConvention + { + public void Apply(ActionModel action) + { + action.Filters.Add(new EnsureUmbracoBackOfficeAuthentication()); + } + } +} diff --git a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs index 11d82d4db5..dc0816e1e2 100644 --- a/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs +++ b/src/Umbraco.Web.Common/ApplicationModels/BackOfficeApplicationModelProvider.cs @@ -17,7 +17,8 @@ namespace Umbraco.Web.Common.ApplicationModels { ActionModelConventions = new List() { - new BackOfficeIdentityCultureConvention() + new BackOfficeIdentityCultureConvention(), + new AuthenticateAsBackOfficeSchemeConvention() }; } diff --git a/src/Umbraco.Web.Common/Filters/EnsureUmbracoBackOfficeAuthentication.cs b/src/Umbraco.Web.Common/Filters/EnsureUmbracoBackOfficeAuthentication.cs new file mode 100644 index 0000000000..5ad43dc922 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/EnsureUmbracoBackOfficeAuthentication.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Web.Common.ApplicationModels; +using Umbraco.Web.Common.Attributes; + +namespace Umbraco.Web.Common.Filters +{ + + /// + /// Assigned as part of the umbraco back office application model + /// to always ensure that back office authentication occurs for all controller/actions with + /// applied. + /// + public class EnsureUmbracoBackOfficeAuthentication : IAuthorizationFilter, IAuthorizeData + { + // Implements IAuthorizeData only to return the back office scheme + public string AuthenticationSchemes { get; set; } = Umbraco.Core.Constants.Security.BackOfficeAuthenticationType; + public string Policy { get; set; } + public string Roles { get; set; } + + public void OnAuthorization(AuthorizationFilterContext context) + { + } + } +} diff --git a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs index 5ddf285601..40f534f289 100644 --- a/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/UmbracoBackOfficeAuthorizeAttribute.cs @@ -6,15 +6,8 @@ namespace Umbraco.Web.Common.Filters /// /// Ensures authorization is successful for a back office user. /// - public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute, IAuthorizeData + public class UmbracoBackOfficeAuthorizeAttribute : TypeFilterAttribute { - // Implements IAuthorizeData to return the back office scheme so that all requests with this attributes - // get authenticated with this scheme. - // TODO: We'll have to refactor this as part of the authz policy changes. - public string AuthenticationSchemes { get; set; } = Umbraco.Core.Constants.Security.BackOfficeAuthenticationType; - public string Policy { get; set; } - public string Roles { get; set; } - /// /// Default constructor /// diff --git a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs index 3d9d1e8622..23ff63a328 100644 --- a/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs +++ b/src/Umbraco.Web.Common/Security/BackofficeSecurity.cs @@ -52,6 +52,18 @@ namespace Umbraco.Web.Common.Security } } + /// + public ValidateRequestAttempt AuthorizeRequest(bool throwExceptions = false) + { + // check for secure connection + if (_globalSettings.UseHttps && !_httpContextAccessor.GetRequiredHttpContext().Request.IsHttps) + { + if (throwExceptions) throw new SecurityException("This installation requires a secure connection (via SSL). Please update the URL to include https://"); + return ValidateRequestAttempt.FailedNoSsl; + } + return ValidateCurrentUser(throwExceptions); + } + /// public Attempt GetUserId() { @@ -94,7 +106,6 @@ namespace Umbraco.Web.Common.Security var user = CurrentUser; - // TODO: All of this is done as part of identity/backofficesigninmanager // Check for console access if (user == null || (requiresApproval && user.IsApproved == false) || (user.IsLockedOut && RequestIsInUmbracoApplication(_httpContextAccessor, _globalSettings, _hostingEnvironment))) {