Merge pull request #1161 from umbraco/temp-U4-4219

U4-4219 Can't Preview protected pages
This commit is contained in:
Shannon Deminick
2016-03-16 10:18:06 +01:00
7 changed files with 246 additions and 42 deletions

View File

@@ -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<UmbracoBackOfficeIdentity>().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);

View File

@@ -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
/// <summary>
/// Configure Default Identity User Manager for Umbraco
/// </summary>
@@ -114,7 +113,24 @@ namespace Umbraco.Web.Security.Identity
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <returns></returns>
/// <remarks>
/// By default this will be configured to execute on PipelineStage.Authenticate
/// </remarks>
public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app, ApplicationContext appContext)
{
return app.UseUmbracoBackOfficeCookieAuthentication(appContext, PipelineStage.Authenticate);
}
/// <summary>
/// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline
/// </summary>
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <param name="stage">
/// Configurable pipeline stage
/// </param>
/// <returns></returns>
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<int>()),
};
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<GetUserSecondsMiddleWare>(
@@ -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;
}
/// <summary>
/// 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
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <returns></returns>
/// <remarks>
/// By default this will be configured to execute on PipelineStage.Authenticate
/// </remarks>
public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app, ApplicationContext appContext)
{
return app.UseUmbracoBackOfficeExternalCookieAuthentication(appContext, PipelineStage.Authenticate);
}
/// <summary>
/// Ensures that the cookie middleware for validating external logins is assigned to the pipeline with the correct
/// Umbraco back office configuration
/// </summary>
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <param name="stage"></param>
/// <returns></returns>
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;
}
/// <summary>
/// In order for preview to work this needs to be called
/// </summary>
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
/// <remarks>
/// By default this will be configured to execute on PipelineStage.PostAuthenticate
/// </remarks>
public static IAppBuilder UseUmbracoPreviewAuthentication(this IAppBuilder app, ApplicationContext appContext)
{
return app.UseUmbracoPreviewAuthentication(appContext, PipelineStage.PostAuthenticate);
}
/// <summary>
/// In order for preview to work this needs to be called
/// </summary>
/// <param name="app"></param>
/// <param name="appContext"></param>
/// <param name="stage"></param>
/// <returns></returns>
/// <remarks>
/// 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.
/// </remarks>
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();
}
/// <summary>
/// Create the default umb cookie auth options
/// </summary>
/// <param name="explicitPaths"></param>
/// <returns></returns>
private static UmbracoBackOfficeCookieAuthOptions CreateCookieAuthOptions(string[] explicitPaths = null)
{
var authOptions = new UmbracoBackOfficeCookieAuthOptions(
explicitPaths,
UmbracoConfig.For.UmbracoSettings().Security,
GlobalSettings.TimeOutInMinutes,
GlobalSettings.UseSSL);
return authOptions;
}
}
}

View File

@@ -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))
{

View File

@@ -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;
/// <summary>
/// Instantiates the middleware with an optional pointer to the next component.
/// </summary>
/// <param name="next"/>
/// <param name="cookieOptions"></param>
public PreviewAuthenticationMiddleware(OwinMiddleware next,
UmbracoBackOfficeCookieAuthOptions cookieOptions) : base(next)
{
_cookieOptions = cookieOptions;
}
/// <summary>
/// Process an individual request.
/// </summary>
/// <param name="context"/>
/// <returns/>
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);
}
}
}
}

View File

@@ -367,6 +367,7 @@
<Compile Include="Media\EmbedProviders\OEmbedPhoto.cs" />
<Compile Include="Security\Identity\IUmbracoBackOfficeTwoFactorOptions.cs" />
<Compile Include="Models\Mapping\PropertyTypeGroupResolver.cs" />
<Compile Include="Security\Identity\PreviewAuthenticationMiddleware.cs" />
<Compile Include="SingletonHttpContextAccessor.cs" />
<Compile Include="Trees\ContentTypeTreeController.cs" />
<Compile Include="Trees\MediaTypeTreeController.cs" />

View File

@@ -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()

View File

@@ -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
/// </remarks>
public class UmbracoDefaultOwinStartup
{
/// <summary>
/// Main startup method
/// </summary>
/// <param name="app"></param>
public virtual void Configuration(IAppBuilder app)
{
app.SanitizeThreadCulture();
ConfigureServices(app);
ConfigureMiddleware(app);
}
/// <summary>
/// Configures services to be created in the OWIN context (CreatePerOwinContext)
/// </summary>
/// <param name="app"></param>
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());
}
/// <summary>
/// Configures middleware to be used (i.e. app.Use...)
/// </summary>
/// <param name="app"></param>
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; }
}
}
}