Migrates preview auth Middleware

Migrates UriExtensionsTests to netcore, fixes preview controller bits, adds tests for preview path for back office route check, fixes virtual paths for views,
This commit is contained in:
Shannon
2020-08-07 00:48:32 +10:00
parent 9cafeaf39e
commit cca7303abd
20 changed files with 257 additions and 235 deletions

View File

@@ -4,23 +4,20 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.Models;
using Umbraco.Web.Features;
using Umbraco.Web.Trees;
namespace Umbraco.Web.Editors
{
// TODO: Almost nothing here needs to exist since we can inject these into the view
public class BackOfficePreviewModel : BackOfficeModel
public class BackOfficePreviewModel
{
private readonly UmbracoFeatures _features;
public IEnumerable<ILanguage> Languages { get; }
public BackOfficePreviewModel(UmbracoFeatures features, IGlobalSettings globalSettings, IUmbracoVersion umbracoVersion, IEnumerable<ILanguage> languages, IContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IRuntimeSettings runtimeSettings, ISecuritySettings securitySettings)
: base(features, globalSettings, umbracoVersion, contentSettings, hostingEnvironment, runtimeSettings, securitySettings)
public BackOfficePreviewModel(UmbracoFeatures features, IEnumerable<ILanguage> languages)
{
_features = features;
Languages = languages;
}
public IEnumerable<ILanguage> Languages { get; }
public bool DisableDevicePreview => _features.Disabled.DisableDevicePreview;
public string PreviewExtendedHeaderView => _features.Enabled.PreviewExtendedView;
}

View File

@@ -76,7 +76,8 @@ namespace Umbraco.Core
}
//check for special back office paths
if (urlPath.InvariantStartsWith("/" + mvcArea + "/" + Constants.Web.Mvc.BackOfficePathSegment + "/"))
if (urlPath.InvariantStartsWith("/" + mvcArea + "/BackOffice/")
|| urlPath.InvariantStartsWith("/" + mvcArea + "/Preview/"))
{
return true;
}

View File

@@ -9,15 +9,6 @@ namespace Umbraco.Web
return cookieManager.GetCookieValue(Constants.Web.PreviewCookieName);
}
/// <summary>
/// Does a preview cookie exist ?
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static bool HasPreviewCookie(this ICookieManager cookieManager)
{
return cookieManager.HasCookie(Constants.Web.PreviewCookieName);
}
}
}

View File

@@ -1,18 +1,31 @@
using System;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web.Hosting;
using Umbraco.Core.Configuration;
using Umbraco.Tests.Common;
using Umbraco.Web.Common.AspNetCore;
namespace Umbraco.Tests.CoreThings
namespace Umbraco.Tests.UnitTests.Umbraco.Core.Extensions
{
[TestFixture]
public class UriExtensionsTests
{
[OneTimeSetUp]
public void Setup()
{
_settingsForTests = new SettingsForTests();
_hostEnvironment = Mock.Of<IWebHostEnvironment>();
_globalSettings = _settingsForTests.GenerateMockGlobalSettings();
}
private SettingsForTests _settingsForTests;
private IWebHostEnvironment _hostEnvironment;
private IGlobalSettings _globalSettings;
[TestCase("http://www.domain.com/umbraco/preview/frame?id=1234", "", true)]
[TestCase("http://www.domain.com/umbraco", "", true)]
[TestCase("http://www.domain.com/Umbraco/", "", true)]
[TestCase("http://www.domain.com/umbraco/default.aspx", "", true)]
@@ -34,13 +47,13 @@ namespace Umbraco.Tests.CoreThings
[TestCase("http://www.domain.com/umbraco/test/legacyAjaxCalls.ashx?some=query&blah=js", "", true)]
public void Is_Back_Office_Request(string input, string virtualPath, bool expected)
{
var globalSettings = SettingsForTests.GenerateMockGlobalSettings();
var mockHostingSettings = Mock.Get(SettingsForTests.GenerateMockHostingSettings());
var mockHostingSettings = Mock.Get(_settingsForTests.GenerateMockHostingSettings());
mockHostingSettings.Setup(x => x.ApplicationVirtualPath).Returns(virtualPath);
var hostingEnvironment = new AspNetHostingEnvironment(mockHostingSettings.Object);
var hostingEnvironment = new AspNetCoreHostingEnvironment(mockHostingSettings.Object, _hostEnvironment);
var source = new Uri(input);
Assert.AreEqual(expected, source.IsBackOfficeRequest(globalSettings, hostingEnvironment));
Assert.AreEqual(expected, source.IsBackOfficeRequest(_globalSettings, hostingEnvironment));
}
[TestCase("http://www.domain.com/install", true)]
@@ -55,7 +68,9 @@ namespace Umbraco.Tests.CoreThings
public void Is_Installer_Request(string input, bool expected)
{
var source = new Uri(input);
Assert.AreEqual(expected, source.IsInstallerRequest(TestHelper.GetHostingEnvironment()));
var mockHostingSettings = Mock.Get(_settingsForTests.GenerateMockHostingSettings());
var hostingEnvironment = new AspNetCoreHostingEnvironment(mockHostingSettings.Object, _hostEnvironment);
Assert.AreEqual(expected, source.IsInstallerRequest(hostingEnvironment));
}
[TestCase("http://www.domain.com/foo/bar", "/", "http://www.domain.com/")]

View File

@@ -432,7 +432,6 @@
</Compile>
<Compile Include="UmbracoExamine\ExamineDemoDataMediaService.cs" />
<Compile Include="UmbracoExamine\ExamineDemoDataContentService.cs" />
<Compile Include="CoreThings\UriExtensionsTests.cs" />
<Compile Include="Composing\PackageActionCollectionTests.cs" />
<Compile Include="Composing\TypeLoaderExtensions.cs" />
<Compile Include="Composing\TypeLoaderTests.cs" />
@@ -561,7 +560,7 @@
<Content Include="Services\Importing\XsltSearch-Package.xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\Mapping" />
<Folder Include="Models\Mapping\" />
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />

View File

@@ -68,7 +68,9 @@ namespace Umbraco.Web.BackOffice.Controllers
[HttpGet]
public async Task<IActionResult> Default()
{
var viewPath = Path.Combine(_globalSettings.UmbracoPath , Umbraco.Core.Constants.Web.Mvc.BackOfficeArea, nameof(Default) + ".cshtml");
var viewPath = Path.Combine(_globalSettings.UmbracoPath , Constants.Web.Mvc.BackOfficeArea, nameof(Default) + ".cshtml")
.Replace("\\", "/"); // convert to forward slashes since it's a virtual path
return await RenderDefaultOrProcessExternalLoginAsync(
() => View(viewPath),
() => View(viewPath));

View File

@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using System;
using System.IO;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration;
@@ -9,6 +10,7 @@ using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.Services;
using Umbraco.Core.WebAssets;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.ActionResults;
using Umbraco.Web.Common.Filters;
@@ -29,14 +31,9 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IGlobalSettings _globalSettings;
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IWebSecurity _webSecurity;
private readonly ILocalizationService _localizationService;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IContentSettings _contentSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILocalizationService _localizationService;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ICookieManager _cookieManager;
private readonly IRuntimeSettings _runtimeSettings;
private readonly ISecuritySettings _securitySettings;
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly ICompositeViewEngine _viewEngines;
@@ -46,13 +43,8 @@ namespace Umbraco.Web.BackOffice.Controllers
IPublishedSnapshotService publishedSnapshotService,
IWebSecurity webSecurity,
ILocalizationService localizationService,
IUmbracoVersion umbracoVersion,
IContentSettings contentSettings,
IHttpContextAccessor httpContextAccessor,
IHostingEnvironment hostingEnvironment,
ICookieManager cookieManager,
IRuntimeSettings settings,
ISecuritySettings securitySettings,
IRuntimeMinifier runtimeMinifier,
ICompositeViewEngine viewEngines)
{
@@ -61,13 +53,8 @@ namespace Umbraco.Web.BackOffice.Controllers
_publishedSnapshotService = publishedSnapshotService;
_webSecurity = webSecurity;
_localizationService = localizationService;
_umbracoVersion = umbracoVersion;
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
_httpContextAccessor = httpContextAccessor;
_hostingEnvironment = hostingEnvironment;
_cookieManager = cookieManager;
_runtimeSettings = settings;
_securitySettings = securitySettings;
_runtimeMinifier = runtimeMinifier;
_viewEngines = viewEngines;
}
@@ -78,7 +65,7 @@ namespace Umbraco.Web.BackOffice.Controllers
{
var availableLanguages = _localizationService.GetAllLanguages();
var model = new BackOfficePreviewModel(_features, _globalSettings, _umbracoVersion, availableLanguages, _contentSettings, _hostingEnvironment, _runtimeSettings, _securitySettings);
var model = new BackOfficePreviewModel(_features, availableLanguages);
if (model.PreviewExtendedHeaderView.IsNullOrWhiteSpace() == false)
{
@@ -87,7 +74,13 @@ namespace Umbraco.Web.BackOffice.Controllers
throw new InvalidOperationException("Could not find the view " + model.PreviewExtendedHeaderView + ", the following locations were searched: " + Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations));
}
return View(_globalSettings.GetBackOfficePath(_hostingEnvironment).EnsureEndsWith('/') + "Views/Preview/" + "Index.cshtml", model);
var viewPath = Path.Combine(
_globalSettings.UmbracoPath,
Constants.Web.Mvc.BackOfficeArea,
ControllerExtensions.GetControllerName<PreviewController>() + ".cshtml")
.Replace("\\", "/"); // convert to forward slashes since it's a virtual path
return View(viewPath, model);
}
/// <summary>
@@ -120,9 +113,8 @@ namespace Umbraco.Web.BackOffice.Controllers
// use a numeric url because content may not be in cache and so .Url would fail
var query = culture.IsNullOrWhiteSpace() ? string.Empty : $"?culture={culture}";
Response.Redirect($"../../{id}.aspx{query}", true);
return null;
return RedirectPermanent($"../../{id}.aspx{query}");
}
public ActionResult End(string redir = null)

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SixLabors.ImageSharp.Web.DependencyInjection;
using Umbraco.Web.BackOffice.Routing;
using Umbraco.Web.BackOffice.Security;
namespace Umbraco.Extensions
{
@@ -27,6 +28,8 @@ namespace Umbraco.Extensions
app.UseImageSharp();
app.UseStaticFiles();
app.UseMiddleware<PreviewAuthenticationMiddleware>();
return app;
}

View File

@@ -27,6 +27,7 @@ namespace Umbraco.Web.BackOffice.Runtime
composition.Register<BackOfficeSessionIdValidator>(Lifetime.Request);
composition.Register<BackOfficeSecurityStampValidator>(Lifetime.Request);
composition.RegisterUnique<PreviewAuthenticationMiddleware>();
composition.RegisterUnique<IBackOfficeAntiforgery, BackOfficeAntiforgery>();
// register back office trees

View File

@@ -20,7 +20,7 @@ namespace Umbraco.Web.BackOffice.Security
/// A custom cookie manager that is used to read the cookie from the request.
/// </summary>
/// <remarks>
/// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install and /base therefore we cannot just set the cookie path to be /umbraco,
/// Umbraco's back office cookie needs to be read on two paths: /umbraco and /install, therefore we cannot just set the cookie path to be /umbraco,
/// instead we'll specify our own cookie manager and return null if the request isn't for an acceptable path.
/// </remarks>
public class BackOfficeCookieManager : ChunkingCookieManager, ICookieManager
@@ -69,7 +69,6 @@ namespace Umbraco.Web.BackOffice.Security
/// We auth the request when:
/// * it is a back office request
/// * it is an installer request
/// * it is a /base request
/// * it is a preview request
/// </remarks>
public bool ShouldAuthenticateRequest(Uri requestUri, bool checkForceAuthTokens = true)
@@ -94,6 +93,7 @@ namespace Umbraco.Web.BackOffice.Security
//check installer
|| requestUri.IsInstallerRequest(_hostingEnvironment))
return true;
return false;
}

View File

@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
namespace Umbraco.Web.BackOffice.Security
{
/// <summary>
/// Ensures that preview pages (front-end routed) are authenticated with the back office identity appended to the principal alongside any default authentication that takes place
/// </summary>
public class PreviewAuthenticationMiddleware : IMiddleware
{
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
public PreviewAuthenticationMiddleware(
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment)
{
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var request = context.Request;
if (!request.IsClientSideRequest())
{
var isPreview = request.HasPreviewCookie()
&& context.User != null
&& !request.IsBackOfficeRequest(_globalSettings, _hostingEnvironment);
if (isPreview)
{
var cookieOptions = context.RequestServices.GetRequiredService<IOptionsSnapshot<CookieAuthenticationOptions>>()
.Get(Constants.Security.BackOfficeAuthenticationType);
if (cookieOptions == null)
throw new InvalidOperationException("No cookie options found with name " + Constants.Security.BackOfficeAuthenticationType);
//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.
if (request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie))
{
var unprotected = cookieOptions.TicketDataFormat.Unprotect(cookie);
if (unprotected != null)
{
var backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity();
if (backOfficeIdentity != 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.
context.User.AddIdentity(backOfficeIdentity);
}
}
}
}
}
await next(context);
}
}
}

View File

@@ -22,7 +22,10 @@ namespace Umbraco.Web.Common.AspNetCore
ApplicationId = AppDomain.CurrentDomain.Id.ToString();
ApplicationPhysicalPath = webHostEnvironment.ContentRootPath;
ApplicationVirtualPath = "/"; //TODO how to find this, This is a server thing, not application thing.
//TODO how to find this, This is a server thing, not application thing.
ApplicationVirtualPath = hostingSettings.ApplicationVirtualPath?.EnsureStartsWith('/')
?? "/";
IISVersion = new Version(0, 0); // TODO not necessary IIS
}

View File

@@ -13,6 +13,17 @@ namespace Umbraco.Extensions
{
public static class HttpRequestExtensions
{
/// <summary>
/// Check if a preview cookie exist
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static bool HasPreviewCookie(this HttpRequest request)
{
return request.Cookies.TryGetValue(Constants.Web.PreviewCookieName, out var cookieVal) && !cookieVal.IsNullOrWhiteSpace();
}
public static bool IsBackOfficeRequest(this HttpRequest request, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
return new Uri(request.GetEncodedUrl(), UriKind.RelativeOrAbsolute).IsBackOfficeRequest(globalSettings, hostingEnvironment);

View File

@@ -61,28 +61,34 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi
}
});
previewHub.client.refreshed = function (message, sender) {
console.log("Notified by SignalR preview hub (" + message + ").");
if (previewHub && previewHub.client) {
previewHub.client.refreshed = function (message, sender) {
console.log("Notified by SignalR preview hub (" + message + ").");
if ($scope.pageId != message) {
console.log("Not a notification for us (" + $scope.pageId + ").");
return;
}
if ($scope.pageId != message) {
console.log("Not a notification for us (" + $scope.pageId + ").");
return;
}
if (!visibleContent) {
console.log("Not visible, will reload.");
dirtyContent = true;
return;
}
if (!visibleContent) {
console.log("Not visible, will reload.");
dirtyContent = true;
return;
}
console.log("Reloading.");
var iframeDoc = (iframe.contentWindow || iframe.contentDocument);
iframeDoc.location.reload();
};
console.log("Reloading.");
var iframeDoc = (iframe.contentWindow || iframe.contentDocument);
iframeDoc.location.reload();
};
}
$.connection.hub.start()
.done(function () { console.log("Connected to SignalR preview hub (ID=" + $.connection.hub.id + ")"); })
.fail(function () { console.log("Could not connect to SignalR preview hub."); });
try {
$.connection.hub.start()
.done(function () { console.log("Connected to SignalR preview hub (ID=" + $.connection.hub.id + ")"); })
.fail(function () { console.log("Could not connect to SignalR preview hub."); });
} catch (e) {
console.error("Could not establish signalr connection. Error: " + e);
}
}
var isInit = getParameterByName("init");

View File

@@ -0,0 +1,89 @@
@using Umbraco.Core
@using Umbraco.Web.WebAssets
@using Umbraco.Web.Common.Security
@using Umbraco.Core.WebAssets
@using Umbraco.Core.Configuration
@using Umbraco.Core.Hosting
@using Umbraco.Extensions
@using Umbraco.Core.Logging
@using Umbraco.Web.BackOffice.Controllers
@inject BackOfficeSignInManager signInManager
@inject BackOfficeServerVariables backOfficeServerVariables
@inject IUmbracoVersion umbracoVersion
@inject IHostingEnvironment hostingEnvironment
@inject IGlobalSettings globalSettings
@inject IRuntimeMinifier runtimeMinifier
@inject IProfilerHtml profilerHtml
@model Umbraco.Web.Editors.BackOfficePreviewModel
@{
var disableDevicePreview = Model.DisableDevicePreview.ToString().ToLowerInvariant();
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Umbraco Preview</title>
<meta name="robots" content="noindex, nofollow">
<meta name="pinterest" content="nopin" />
@Html.Raw(await runtimeMinifier.RenderCssHereAsync(BackOfficeWebAssets.UmbracoPreviewCssBundleName))
</head>
<body id="canvasdesignerPanel" ng-mouseover="outlinePositionHide()" ng-controller="previewController" ng-class="{'tabbing-active': tabbingActive === true}" ng-click="windowClickHandler($event)">
<div class="wait" ng-show="!frameLoaded"></div>
@if (!string.IsNullOrWhiteSpace(Model.PreviewExtendedHeaderView))
{
@await Html.PartialAsync(Model.PreviewExtendedHeaderView)
}
<div id="demo-iframe-wrapper" class="{{previewDevice.css}}">
<preview-i-frame src="pageUrl" on-loaded="onFrameLoaded(iframe)"></preview-i-frame>
</div>
<div class="canvasdesigner" ng-init="showDevicesPreview = true; showDevices = !@(disableDevicePreview);" ng-mouseenter="positionSelectedHide()">
<div class="menu-bar selected">
<div class="menu-bar__title">Preview Mode</div>
<div class="menu-bar__right-part">
<div class="preview-menu-option" ng-class="{'--open': sizeOpen === true}" ng-click="$event.stopPropagation()">
<button class="menu-bar__button umb-outline" ng-click="toggleSizeOpen()"><i class="icon {{previewDevice.icon}}"></i><span>{{previewDevice.title}}</span></button>
<div class="dropdown-menu">
<button ng-repeat="device in devices" class="menu-bar__button umb-outline" ng-class="{ '--active':previewDevice === device }" ng-click="updatePreviewDevice(device)">
<i class="icon {{device.icon}}"></i><span>{{device.title}}</span>
</button>
</div>
</div>
@if (Model.Languages != null && Model.Languages.Count() > 1)
{
<div class="preview-menu-option" ng-class="{'--open': cultureOpen === true}" ng-click="$event.stopPropagation()">
<button class="menu-bar__button umb-outline" ng-click="toggleCultureOpen()"><i class="icon icon-globe-europe---africa"></i><span>{{currentCulture.title}}</span></button>
<div class="dropdown-menu">
@foreach (var language in Model.Languages)
{
<button class="menu-bar__button umb-outline" ng-class="{ '--active': currentCultureIso === '@language.IsoCode' || (@language.IsDefault.ToString().ToLower() && currentCultureIso === null) }" ng-click="changeCulture('@language.IsoCode')" ng-init="registerCulture('@language.IsoCode', '@language.CultureName', @language.IsDefault.ToString().ToLower())">
<i class="icon icon-globe-europe---africa"></i><span>@language.CultureName</span>
</button>
}
</div>
</div>
}
<button ng-click="exitPreview()" title="Exit Preview" class="menu-bar__button umb-outline">
<i class="icon icon-power"></i><span>Exit</span>
</button>
</div>
</div>
</div>
<script src="../lib/lazyload-js/lazyload.min.js"></script>
<script src="@Url.GetUrlWithCacheBust("Application", "Preview", null, hostingEnvironment, umbracoVersion, runtimeMinifier)"></script>
</body>
</html>

View File

@@ -97,16 +97,6 @@ namespace Umbraco.Web
return null;
}
/// <summary>
/// Does a preview cookie exist ?
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public static bool HasPreviewCookie(this IOwinRequest request)
{
return request.Cookies[Constants.Web.PreviewCookieName] != null;
}
/// <summary>
/// Returns the cookie's string value
/// </summary>

View File

@@ -1,24 +1,14 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Owin;
using Microsoft.Owin.Extensions;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.DataHandler;
using Microsoft.Owin.Security.DataProtection;
using Owin;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.Mapping;
using Umbraco.Net;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Constants = Umbraco.Core.Constants;
@@ -84,65 +74,6 @@ namespace Umbraco.Web.Security
return app;
}
/// <summary>
/// In order for preview to work this needs to be called
/// </summary>
/// <param name="app"></param>
/// <param name="umbracoContextAccessor"></param>
/// <param name="runtimeState"></param>
/// <param name="globalSettings"></param>
/// <param name="securitySettings"></param>
/// <param name="ioHelper"></param>
/// <param name="hostingEnvironment"></param>
/// <param name="requestCache"></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, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache)
{
return app.UseUmbracoPreviewAuthentication(umbracoContextAccessor, runtimeState, globalSettings, securitySettings, hostingEnvironment, requestCache, PipelineStage.PostAuthenticate);
}
/// <summary>
/// In order for preview to work this needs to be called
/// </summary>
/// <param name="app"></param>
/// <param name="umbracoContextAccessor"></param>
/// <param name="runtimeState"></param>
/// <param name="globalSettings"></param>
/// <param name="securitySettings"></param>
/// <param name="ioHelper"></param>
/// <param name="hostingEnvironment"></param>
/// <param name="requestCache"></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, IUmbracoContextAccessor umbracoContextAccessor, IRuntimeState runtimeState, IGlobalSettings globalSettings, ISecuritySettings securitySettings, IHostingEnvironment hostingEnvironment, IRequestCache requestCache, PipelineStage stage)
{
if (runtimeState.Level != RuntimeLevel.Run) return app;
//var authOptions = app.CreateUmbracoCookieAuthOptions(umbracoContextAccessor, globalSettings, runtimeState, securitySettings, hostingEnvironment, requestCache);
app.Use(typeof(PreviewAuthenticationMiddleware), /*authOptions*/null, globalSettings, hostingEnvironment);
// This middleware must execute at least on PostAuthentication, by default it is on Authorize
// The middleware needs to execute after the RoleManagerModule executes which is during PostAuthenticate,
// currently I've had 100% success with ensuring this fires after RoleManagerModule even if this is set
// to PostAuthenticate though not sure if that's always a guarantee so by default it's Authorize.
if (stage < PipelineStage.PostAuthenticate)
throw new InvalidOperationException("The stage specified for UseUmbracoPreviewAuthentication must be greater than or equal to " + PipelineStage.PostAuthenticate);
app.UseStageMarker(stage);
return app;
}
public static void SanitizeThreadCulture(this IAppBuilder app)
{
Thread.CurrentThread.SanitizeThreadCulture();

View File

@@ -1,78 +0,0 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin;
using Umbraco.Core;
using Umbraco.Core.BackOffice;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
namespace Umbraco.Web.Security
{
internal class PreviewAuthenticationMiddleware : OwinMiddleware
{
private readonly UmbracoBackOfficeCookieAuthOptions _cookieOptions;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
/// <summary>
/// Instantiates the middleware with an optional pointer to the next component.
/// </summary>
/// <param name="next"/>
/// <param name="cookieOptions"></param>
/// <param name="globalSettings"></param>
/// <param name="hostingEnvironment"></param>
public PreviewAuthenticationMiddleware(OwinMiddleware next,
UmbracoBackOfficeCookieAuthOptions cookieOptions, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment) : base(next)
{
_cookieOptions = cookieOptions;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
}
/// <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(_globalSettings, _hostingEnvironment) == 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.
if (!UmbracoBackOfficeIdentity.FromClaimsIdentity(unprotected.Identity, out var umbracoIdentity))
throw new InvalidOperationException("Cannot convert identity");
claimsPrincipal.AddIdentity(umbracoIdentity);
}
}
}
}
if (Next != null)
{
await Next.Invoke(context);
}
}
}
}

View File

@@ -261,7 +261,6 @@
<Compile Include="Security\ForceRenewalCookieAuthenticationMiddleware.cs" />
<Compile Include="Security\GetUserSecondsMiddleWare.cs" />
<Compile Include="Security\IUmbracoBackOfficeTwoFactorOptions.cs" />
<Compile Include="Security\PreviewAuthenticationMiddleware.cs" />
<Compile Include="Security\WebAuthExtensions.cs" />
<Compile Include="WebAssets\CDF\UmbracoClientDependencyLoader.cs" />
<Compile Include="UmbracoDefaultOwinStartup.cs" />
@@ -410,7 +409,7 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="ContentApps" />
<Folder Include="ContentApps\" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
</Project>
</Project>

View File

@@ -89,10 +89,7 @@ namespace Umbraco.Web
// Ensure owin is configured for Umbraco back office authentication.
// Front-end OWIN cookie configuration must be declared after this code.
app
// already moved to netcore
//.UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate)
.UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate)
.UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, SecuritySettings, HostingEnvironment, RequestCache, PipelineStage.Authorize);
.UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, HostingEnvironment, RequestCache, PipelineStage.Authenticate);
}
public static event EventHandler<OwinMiddlewareConfiguredEventArgs> MiddlewareConfigured;