Merge pull request #8603 from umbraco/netcore/feature/6976-migrate-preview-middleware

Migrates preview auth Middleware
This commit is contained in:
Bjarke Berg
2020-08-11 08:24:57 +02:00
committed by GitHub
22 changed files with 259 additions and 250 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

@@ -429,7 +429,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" />
@@ -558,6 +557,7 @@
<Content Include="Services\Importing\XsltSearch-Package.xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Models\Mapping\" />
<Folder Include="Models\Mapping" />
<Folder Include="Web\Validation" />
</ItemGroup>

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

@@ -40,17 +40,13 @@ namespace Umbraco.Web.Editors
public class BackOfficeController : UmbracoController
{
private readonly UmbracoFeatures _features;
private readonly IRuntimeState _runtimeState;
private BackOfficeOwinUserManager _userManager;
private BackOfficeSignInManager _signInManager;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IGridConfig _gridConfig;
private readonly IContentSettings _contentSettings;
private readonly TreeCollection _treeCollection;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IRuntimeSettings _runtimeSettings;
private readonly ISecuritySettings _securitySettings;
private readonly IRuntimeMinifier _runtimeMinifier;
public BackOfficeController(
UmbracoFeatures features,
@@ -59,29 +55,20 @@ namespace Umbraco.Web.Editors
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IRuntimeState runtimeState,
IUmbracoVersion umbracoVersion,
IGridConfig gridConfig,
IContentSettings contentSettings,
TreeCollection treeCollection,
IHostingEnvironment hostingEnvironment,
IHttpContextAccessor httpContextAccessor,
IRuntimeSettings settings,
ISecuritySettings securitySettings,
IRuntimeMinifier runtimeMinifier)
ISecuritySettings securitySettings)
: base(globalSettings, umbracoContextAccessor, services, appCaches, profilingLogger)
{
_features = features;
_runtimeState = runtimeState;
_umbracoVersion = umbracoVersion;
_gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig));
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
_treeCollection = treeCollection ?? throw new ArgumentNullException(nameof(treeCollection));
_hostingEnvironment = hostingEnvironment;
_runtimeSettings = settings;
_securitySettings = securitySettings;
_runtimeMinifier = runtimeMinifier;
}
protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager());

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

@@ -25,6 +25,7 @@
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>latest</LangVersion>
<CodeAnalysisRuleSet>..\UnusedCode.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>portable</DebugType>
@@ -262,7 +263,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,5 +410,6 @@
<Link>Mvc\web.config</Link>
</None>
</ItemGroup>
<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;

View File

@@ -18,9 +18,6 @@ namespace Umbraco.Web
/// </summary>
public static class UrlHelperRenderExtensions
{
private static readonly IHtmlString EmptyHtmlString = new HtmlString(string.Empty);
// #region GetCropUrl
//
// /// <summary>