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..ba3d79447a 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 /// @@ -114,7 +113,24 @@ namespace Umbraco.Web.Security.Identity /// /// /// + /// + /// By default this will be configured to execute on PipelineStage.Authenticate + /// public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoBackOfficeCookieAuthentication(appContext, PipelineStage.Authenticate); + } + + /// + /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline + /// + /// + /// + /// + /// Configurable pipeline stage + /// + /// + public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, ApplicationContext appContext, PipelineStage stage) { if (app == null) throw new ArgumentNullException("app"); if (appContext == null) throw new ArgumentNullException("appContext"); @@ -131,28 +147,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); + app.UseUmbracoBackOfficeCookieAuthentication(authOptions, appContext, stage); //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( @@ -164,7 +170,7 @@ namespace Umbraco.Web.Security.Identity return app; } - internal static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, ApplicationContext appContext) + internal static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, CookieAuthenticationOptions options, ApplicationContext appContext, PipelineStage stage = PipelineStage.Authenticate) { if (app == null) { @@ -173,22 +179,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(stage); return app; } + /// /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct /// Umbraco back office configuration @@ -196,7 +201,23 @@ namespace Umbraco.Web.Security.Identity /// /// /// + /// + /// By default this will be configured to execute on PipelineStage.Authenticate + /// public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoBackOfficeExternalCookieAuthentication(appContext, PipelineStage.Authenticate); + } + + /// + /// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct + /// Umbraco back office configuration + /// + /// + /// + /// + /// + public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, ApplicationContext appContext, PipelineStage stage) { if (app == null) throw new ArgumentNullException("app"); if (appContext == null) throw new ArgumentNullException("appContext"); @@ -213,15 +234,81 @@ namespace Umbraco.Web.Security.Identity CookieSecure = GlobalSettings.UseSSL ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest, CookieHttpOnly = true, CookieDomain = UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain - }); + }, stage); + + return app; + } + + /// + /// 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. + /// + /// + /// By default this will be configured to execute on PipelineStage.PostAuthenticate + /// + public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, ApplicationContext appContext) + { + return app.UseUmbracoPreviewAuthentication(appContext, PipelineStage.PostAuthenticate); + } + + /// + /// 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, PipelineStage stage) + { + //don't apply if app isnot ready + if (appContext.IsUpgrading || appContext.IsConfigured) + { + var authOptions = CreateCookieAuthOptions(); + app.Use(typeof(PreviewAuthenticationMiddleware), authOptions); + + if (stage < PipelineStage.PostAuthenticate) + { + throw new InvalidOperationException("The stage specified for UseUmbracoPreviewAuthentication must be greater than or equal to " + PipelineStage.PostAuthenticate); + } + + //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(stage); + } return app; } - #endregion 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 65fc475c0b..9778cee6e0 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -367,6 +367,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..1f1f1c7674 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; @@ -19,23 +21,50 @@ namespace Umbraco.Web /// public class UmbracoDefaultOwinStartup { + /// + /// Main startup method + /// + /// public virtual void Configuration(IAppBuilder app) { app.SanitizeThreadCulture(); + ConfigureServices(app); + ConfigureMiddleware(app); + } + + /// + /// Configures services to be created in the OWIN context (CreatePerOwinContext) + /// + /// + protected virtual void ConfigureServices(IAppBuilder app) + { app.SetUmbracoLoggerFactory(); //Configure the Identity user manager for use with Umbraco Back office // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( - ApplicationContext.Current, + ApplicationContext, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + } + /// + /// Configures middleware to be used (i.e. app.Use...) + /// + /// + protected virtual void ConfigureMiddleware(IAppBuilder app) + { //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN // cookie configuration, this must be declared after it. app - .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext.Current) - .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext.Current); + .UseUmbracoBackOfficeCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoBackOfficeExternalCookieAuthentication(ApplicationContext, PipelineStage.Authenticate) + .UseUmbracoPreviewAuthentication(ApplicationContext, PipelineStage.PostAuthenticate); + } + + protected virtual ApplicationContext ApplicationContext + { + get { return ApplicationContext.Current; } } } }