From 22385d40dbbc9ee92dee3fa44203497ecad37130 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 9 Mar 2016 17:35:50 +0100 Subject: [PATCH] U4-4219 Can't Preview protected pages --- .../Security/AuthenticationExtensions.cs | 10 ++- .../Security/Identity/AppBuilderExtensions.cs | 79 +++++++++++------ .../Identity/BackOfficeCookieManager.cs | 4 +- .../PreviewAuthenticationMiddleware.cs | 84 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/UmbracoContext.cs | 9 +- src/Umbraco.Web/UmbracoDefaultOwinStartup.cs | 8 +- 7 files changed, 159 insertions(+), 36 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index b8064474bb..524663c9c5 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -103,10 +103,18 @@ namespace Umbraco.Core.Security var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; if (backOfficeIdentity != null) return backOfficeIdentity; + //Check if there's more than one identity assigned and see if it's a UmbracoBackOfficeIdentity and use that + var claimsPrincipal = http.User as ClaimsPrincipal; + if (claimsPrincipal != null) + { + backOfficeIdentity = claimsPrincipal.Identities.OfType().FirstOrDefault(); + if (backOfficeIdentity != null) return backOfficeIdentity; + } + //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; if (claimsIdentity != null && claimsIdentity.IsAuthenticated) - { + { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 4420e1983b..6eadc4eb17 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Threading; using System.Web; using Microsoft.AspNet.Identity; @@ -31,9 +32,7 @@ namespace Umbraco.Web.Security.Identity { app.SetLoggerFactory(new OwinLoggerFactory()); } - - #region Backoffice - + /// /// Configure Default Identity User Manager for Umbraco /// @@ -131,28 +130,18 @@ namespace Umbraco.Web.Security.Identity identity => identity.GetUserId()), }; - var authOptions = new UmbracoBackOfficeCookieAuthOptions( - UmbracoConfig.For.UmbracoSettings().Security, - GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL) - { - Provider = cookieAuthProvider - }; + var authOptions = CreateCookieAuthOptions(); + authOptions.Provider = cookieAuthProvider; app.UseUmbracoBackOfficeCookieAuthentication(authOptions, appContext); //don't apply if app isnot ready if (appContext.IsUpgrading || appContext.IsConfigured) { - var getSecondsOptions = new UmbracoBackOfficeCookieAuthOptions( + var getSecondsOptions = CreateCookieAuthOptions( //This defines the explicit path read cookies from for this middleware - new[]{string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path)}, - UmbracoConfig.For.UmbracoSettings().Security, - GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL) - { - Provider = cookieAuthProvider - }; + new[] {string.Format("{0}/backoffice/UmbracoApi/Authentication/GetRemainingTimeoutSeconds", GlobalSettings.Path)}); + getSecondsOptions.Provider = cookieAuthProvider; //This is a custom middleware, we need to return the user's remaining logged in seconds app.Use( @@ -173,21 +162,21 @@ namespace Umbraco.Web.Security.Identity //First the normal cookie middleware app.Use(typeof(CookieAuthenticationMiddleware), app, options); - app.UseStageMarker(PipelineStage.Authenticate); - //don't apply if app isnot ready if (appContext.IsUpgrading || appContext.IsConfigured) { //Then our custom middlewares app.Use(typeof(ForceRenewalCookieAuthenticationMiddleware), app, options, new SingletonUmbracoContextAccessor()); - app.UseStageMarker(PipelineStage.Authenticate); - app.Use(typeof(FixWindowsAuthMiddlware)); - app.UseStageMarker(PipelineStage.Authenticate); + app.Use(typeof(FixWindowsAuthMiddlware)); } + //Marks all of the above middlewares to execute on Authenticate + app.UseStageMarker(PipelineStage.Authenticate); + return app; } + /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct @@ -217,11 +206,53 @@ namespace Umbraco.Web.Security.Identity return app; } - #endregion + + /// + /// In order for preview to work this needs to be called + /// + /// + /// + /// + /// + /// This ensures that during a preview request that the back office use is also Authenticated and that the back office Identity + /// is added as a secondary identity to the current IPrincipal so it can be used to Authorize the previewed document. + /// + public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + //don't apply if app isnot ready + if (appContext.IsUpgrading || appContext.IsConfigured) + { + var authOptions = CreateCookieAuthOptions(); + app.Use(typeof(PreviewAuthenticationMiddleware), authOptions); + + //Marks the above middlewares to execute on PostAuthenticate + //NOTE: The above middleware needs to execute after the RoleManagerModule executes which is also during PostAuthenticate, + // currently I've had 100% success with ensuring this fires after RoleManagerModule though not sure if that's always a + // guarantee. + app.UseStageMarker(PipelineStage.PostAuthenticate); + } + + return app; + } public static void SanitizeThreadCulture(this IAppBuilder app) { Thread.CurrentThread.SanitizeThreadCulture(); } + + /// + /// Create the default umb cookie auth options + /// + /// + /// + private static UmbracoBackOfficeCookieAuthOptions CreateCookieAuthOptions(string[] explicitPaths = null) + { + var authOptions = new UmbracoBackOfficeCookieAuthOptions( + explicitPaths, + UmbracoConfig.For.UmbracoSettings().Security, + GlobalSettings.TimeOutInMinutes, + GlobalSettings.UseSSL); + return authOptions; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs index 598309161e..07f4a3317f 100644 --- a/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs +++ b/src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs @@ -101,9 +101,7 @@ namespace Umbraco.Web.Security.Identity //check back office || request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) //check installer - || request.Uri.IsInstallerRequest() - //detect in preview - || (request.HasPreviewCookie() && request.Uri != null && request.Uri.AbsolutePath.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false) + || request.Uri.IsInstallerRequest() //check for base || BaseRest.BaseRestHandler.IsBaseRestRequest(originalRequestUrl)) { diff --git a/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs new file mode 100644 index 0000000000..7ab071ecea --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/PreviewAuthenticationMiddleware.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; +using Microsoft.Owin.Logging; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Security.Identity +{ + internal class PreviewAuthenticationMiddleware : OwinMiddleware + { + private readonly UmbracoBackOfficeCookieAuthOptions _cookieOptions; + + /// + /// Instantiates the middleware with an optional pointer to the next component. + /// + /// + /// + public PreviewAuthenticationMiddleware(OwinMiddleware next, + UmbracoBackOfficeCookieAuthOptions cookieOptions) : base(next) + { + _cookieOptions = cookieOptions; + } + + /// + /// Process an individual request. + /// + /// + /// + public override async Task Invoke(IOwinContext context) + { + var request = context.Request; + if (request.Uri.IsClientSideRequest() == false) + { + var claimsPrincipal = context.Request.User as ClaimsPrincipal; + var isPreview = request.HasPreviewCookie() + && claimsPrincipal != null + && request.Uri != null + && request.Uri.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false; + if (isPreview) + { + //If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. + // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication + // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. + + var cookie = request.Cookies[_cookieOptions.CookieName]; + if (cookie.IsNullOrWhiteSpace() == false) + { + var unprotected = _cookieOptions.TicketDataFormat.Unprotect(cookie); + if (unprotected != null) + { + //Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Principal, this means we'll have 2 identities assigned to the principal which we can + // use to authorize the preview and allow for a back office User. + claimsPrincipal.AddIdentity(UmbracoBackOfficeIdentity.FromClaimsIdentity(unprotected.Identity)); + } + } + } + } + + if (Next != null) + { + await Next.Invoke(context); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index cb09ef97fd..a6e8274769 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -366,6 +366,7 @@ + diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 6bd325c275..8d56a81084 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -466,14 +466,11 @@ namespace Umbraco.Web var request = GetRequestFromContext(); if (request == null || request.Url == null) return false; - - var currentUrl = request.Url.AbsolutePath; - // zb-00004 #29956 : refactor cookies names & handling + return - //StateHelper.Cookies.Preview.HasValue // has preview cookie HttpContext.Request.HasPreviewCookie() - && currentUrl.StartsWith(IOHelper.ResolveUrl(SystemDirectories.Umbraco)) == false - && UmbracoUser != null; // has user + && request.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath) == false + && Security.CurrentUser != null; // has user } private HttpRequestBase GetRequestFromContext() diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index b810d515d2..dad4fc3aa3 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -1,4 +1,6 @@ -using Microsoft.Owin; +using System.Web; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; using Microsoft.Owin.Logging; using Owin; using Umbraco.Core; @@ -35,7 +37,9 @@ namespace Umbraco.Web // cookie configuration, this must be declared after it. app .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current) - .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current); + .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current) + .UseUmbracoPreviewAuthentication(ApplicationContext.Current); + } } }