Uses correct preview cookie same site and secure settings to allow preview mode to flow between links in the preview frame (#18640)
* Uses correct preview cookie same site and secure settings to allow preview mode to flow between links in the preview frame. * Fixed comment.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
@@ -34,7 +34,7 @@ public class PreviewService : IPreviewService
|
||||
|
||||
if (attempt.Success)
|
||||
{
|
||||
_cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, true);
|
||||
_cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, true, true, "None");
|
||||
}
|
||||
|
||||
return attempt.Success;
|
||||
|
||||
@@ -1,15 +1,61 @@
|
||||
namespace Umbraco.Cms.Core.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Defines cookie related operations.
|
||||
/// </summary>
|
||||
public interface ICookieManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Expires the cookie with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
void ExpireCookie(string cookieName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the cookie with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
string? GetCookieValue(string cookieName);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a cookie with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
/// <param name="value">The cookie value.</param>
|
||||
[Obsolete("Use overload with the httpOnly parameter instead. Scheduled for removal in V16.")]
|
||||
void SetCookieValue(string cookieName, string value) => SetCookieValue(cookieName, value, false);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a cookie with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
/// <param name="value">The cookie value.</param>
|
||||
/// <param name="httpOnly">Indicates whether the created cookie should be marked as HTTP only.</param>
|
||||
[Obsolete("Use overload with the secure and sameSiteMode parameters instead. Scheduled for removal in V17.")]
|
||||
void SetCookieValue(string cookieName, string value, bool httpOnly);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of a cookie with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
/// <param name="value">The cookie value.</param>
|
||||
/// <param name="httpOnly">Indicates whether the created cookie should be marked as HTTP only.</param>
|
||||
/// <param name="secure">Indicates whether the created cookie should be marked as secure.</param>
|
||||
/// <param name="sameSiteMode">Indicates the created cookie's same site status.</param>
|
||||
/// <remarks>
|
||||
/// The value provided by <paramref name="sameSiteMode"/> should match the enum values available from
|
||||
/// Microsoft.AspNetCore.Http.SameSiteMode.
|
||||
/// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in
|
||||
/// the core project.
|
||||
/// </remarks>
|
||||
void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode)
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
=> SetCookieValue(cookieName, value, httpOnly);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether a cookie with the specified name exists.
|
||||
/// </summary>
|
||||
/// <param name="cookieName">The cookie name.</param>
|
||||
bool HasCookie(string cookieName);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.Install
|
||||
{
|
||||
installId = Guid.NewGuid();
|
||||
|
||||
_cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString(), false);
|
||||
_cookieManager.SetCookieValue(Constants.Web.InstallerCookieName, installId.ToString(), false, false, "Unspecified");
|
||||
}
|
||||
|
||||
var dbProvider = string.Empty;
|
||||
|
||||
@@ -3,13 +3,21 @@ using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.AspNetCore;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="ICookieManager"/> implementation for ASP.NET Core.
|
||||
/// </summary>
|
||||
public class AspNetCoreCookieManager : ICookieManager
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AspNetCoreCookieManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="httpContextAccessor">The <see href="IHttpContextAccessor" />.</param>
|
||||
public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) =>
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void ExpireCookie(string cookieName)
|
||||
{
|
||||
HttpContext? httpContext = _httpContextAccessor.HttpContext;
|
||||
@@ -21,14 +29,43 @@ public class AspNetCoreCookieManager : ICookieManager
|
||||
|
||||
var cookieValue = httpContext.Request.Cookies[cookieName];
|
||||
|
||||
httpContext.Response.Cookies.Append(cookieName, cookieValue ?? string.Empty,
|
||||
new CookieOptions { Expires = DateTime.Now.AddYears(-1) });
|
||||
httpContext.Response.Cookies.Append(
|
||||
cookieName,
|
||||
cookieValue ?? string.Empty,
|
||||
new CookieOptions
|
||||
{
|
||||
Expires = DateTime.Now.AddYears(-1),
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName];
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use overload with the secure and sameSiteMode parameters instead. Scheduled for removal in V17.")]
|
||||
public void SetCookieValue(string cookieName, string value, bool httpOnly) =>
|
||||
_httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, new CookieOptions { HttpOnly = httpOnly });
|
||||
SetCookieValue(cookieName, value, httpOnly, false, SameSiteMode.Unspecified.ToString());
|
||||
|
||||
public bool HasCookie(string cookieName) => !(GetCookieValue(cookieName) is null);
|
||||
/// <inheritdoc/>
|
||||
public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode)
|
||||
{
|
||||
SameSiteMode sameSiteModeValue = ParseSameSiteMode(sameSiteMode);
|
||||
_httpContextAccessor.HttpContext?.Response.Cookies.Append(
|
||||
cookieName,
|
||||
value,
|
||||
new CookieOptions
|
||||
{
|
||||
HttpOnly = httpOnly,
|
||||
SameSite = sameSiteModeValue,
|
||||
Secure = secure,
|
||||
});
|
||||
}
|
||||
|
||||
private static SameSiteMode ParseSameSiteMode(string sameSiteMode) =>
|
||||
Enum.TryParse(sameSiteMode, ignoreCase: true, out SameSiteMode result)
|
||||
? result
|
||||
: throw new ArgumentException($"The provided {nameof(sameSiteMode)} value could not be parsed into as SameSiteMode value.", nameof(sameSiteMode));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasCookie(string cookieName) => GetCookieValue(cookieName) is not null;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ public class WebProfiler : IProfiler
|
||||
&& !location.Contains("://"))
|
||||
{
|
||||
MiniProfilerContext.Value.Root.Name = "Before Redirect";
|
||||
cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson(), false);
|
||||
cookieManager.SetCookieValue(WebProfileCookieKey, MiniProfilerContext.Value.ToJson(), false, false, "Unspecified");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Preview;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
||||
|
||||
[TestFixture]
|
||||
public class PreviewServiceTests
|
||||
{
|
||||
[Test]
|
||||
public async Task TryEnterPreviewAsync_Sets_Expected_Cookie_On_Successful_Token_Generation()
|
||||
{
|
||||
var userKey = Guid.NewGuid();
|
||||
const string Token = "token";
|
||||
|
||||
var cookieManagerMock = new Mock<ICookieManager>();
|
||||
|
||||
var previewTokenGeneratorMock = new Mock<IPreviewTokenGenerator>();
|
||||
previewTokenGeneratorMock
|
||||
.Setup(x => x.GenerateTokenAsync(It.Is<Guid>(y => y == userKey)))
|
||||
.ReturnsAsync(Attempt<string?>.Succeed(Token));
|
||||
|
||||
var previewService = new PreviewService(
|
||||
cookieManagerMock.Object,
|
||||
previewTokenGeneratorMock.Object,
|
||||
Mock.Of<IServiceScopeFactory>(),
|
||||
Mock.Of<IRequestCache>());
|
||||
|
||||
var user = new UserBuilder()
|
||||
.WithKey(userKey)
|
||||
.Build();
|
||||
|
||||
var result = await previewService.TryEnterPreviewAsync(user);
|
||||
|
||||
cookieManagerMock
|
||||
.Verify(x => x.SetCookieValue(
|
||||
It.Is<string>(y => y == Constants.Web.PreviewCookieName),
|
||||
It.Is<string>(y => y == Token),
|
||||
It.Is<bool>(y => y == true),
|
||||
It.Is<bool>(y => y == true),
|
||||
It.Is<string>(y => y == SameSiteMode.None.ToString())));
|
||||
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Web.Common.AspNetCore;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Extensions;
|
||||
|
||||
[TestFixture]
|
||||
public class AspNetCoreCookieManagerTests
|
||||
{
|
||||
private const string CookieName = "testCookie";
|
||||
private const string CookieValue = "testValue";
|
||||
|
||||
[Test]
|
||||
public void Can_Set_Cookie()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var cookieManager = CreateCookieManager(httpContext);
|
||||
|
||||
cookieManager.SetCookieValue(CookieName, CookieValue, true, true, "Strict");
|
||||
|
||||
Assert.AreEqual(GetExpectedCookie(), httpContext.Response.Headers.SetCookie);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Set_Cookie_With_Invalid_Same_Site_Value_Throws_Expected_Exception()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
var cookieManager = CreateCookieManager(httpContext);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => cookieManager.SetCookieValue(CookieName, CookieValue, true, true, "invalid"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Get_Cookie()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
AddCookieToRequest(httpContext);
|
||||
var cookieManager = CreateCookieManager(httpContext);
|
||||
|
||||
var result = cookieManager.GetCookieValue(CookieName);
|
||||
|
||||
Assert.AreEqual(CookieValue, result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Verify_Cookie_Exists()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
AddCookieToRequest(httpContext);
|
||||
var cookieManager = CreateCookieManager(httpContext);
|
||||
|
||||
var result = cookieManager.HasCookie(CookieName);
|
||||
|
||||
Assert.IsTrue(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Expire_Cookie()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
AddCookieToRequest(httpContext);
|
||||
var cookieManager = CreateCookieManager(httpContext);
|
||||
|
||||
cookieManager.SetCookieValue(CookieName, CookieValue, true, true, "Strict");
|
||||
cookieManager.ExpireCookie(CookieName);
|
||||
|
||||
var setCookieHeader = httpContext.Response.Headers.SetCookie.ToString();
|
||||
Assert.IsTrue(setCookieHeader.StartsWith(GetExpectedCookie()));
|
||||
Assert.IsTrue(setCookieHeader.Contains($"expires="));
|
||||
}
|
||||
|
||||
private static AspNetCoreCookieManager CreateCookieManager(DefaultHttpContext httpContext)
|
||||
{
|
||||
var httpContextAccessor = Mock.Of<IHttpContextAccessor>(x => x.HttpContext == httpContext);
|
||||
return new AspNetCoreCookieManager(httpContextAccessor);
|
||||
}
|
||||
|
||||
private static void AddCookieToRequest(DefaultHttpContext httpContext)
|
||||
{
|
||||
var cookie = new StringValues(CookieName + "=" + CookieValue);
|
||||
httpContext.Request.Headers.Append(HeaderNames.Cookie, cookie);
|
||||
}
|
||||
|
||||
private static string GetExpectedCookie() => $"testCookie={CookieValue}; path=/; secure; samesite=strict; httponly";
|
||||
}
|
||||
Reference in New Issue
Block a user