Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/Security/CookiePolicy/src/CookiePolicyOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Builder;
/// </summary>
public class CookiePolicyOptions
{
private string _consentCookieValue = "yes";

/// <summary>
/// Affects the cookie's same site attribute.
/// </summary>
Expand All @@ -37,6 +39,24 @@ public class CookiePolicyOptions
IsEssential = true,
};

/// <summary>
/// Gets or sets the value for the cookie used to track if the user consented to the
/// cookie use policy.
/// </summary>
/// <value>Defaults to <c>yes</c>.</value>
public string ConsentCookieValue
{
get => _consentCookieValue;
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Value cannot be null or empty string.", nameof(value));
}
_consentCookieValue = value;
}
}

/// <summary>
/// Checks if consent policies should be evaluated on this request. The default is false.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Security/CookiePolicy/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
Microsoft.AspNetCore.Builder.CookiePolicyOptions.ConsentCookieValue.get -> string!
Microsoft.AspNetCore.Builder.CookiePolicyOptions.ConsentCookieValue.set -> void
7 changes: 3 additions & 4 deletions src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace Microsoft.AspNetCore.CookiePolicy;

internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
{
private const string ConsentValue = "yes";
private readonly ILogger _logger;
private bool? _isConsentNeeded;
private bool? _hasConsent;
Expand Down Expand Up @@ -55,7 +54,7 @@ public bool HasConsent
if (!_hasConsent.HasValue)
{
var cookie = Context.Request.Cookies[Options.ConsentCookie.Name!];
_hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
_hasConsent = string.Equals(cookie, Options.ConsentCookieValue, StringComparison.Ordinal);
_logger.HasConsent(_hasConsent.Value);
}

Expand All @@ -71,7 +70,7 @@ public void GrantConsent()
{
var cookieOptions = Options.ConsentCookie.Build(Context);
// Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply.
Append(Options.ConsentCookie.Name!, ConsentValue, cookieOptions);
Append(Options.ConsentCookie.Name!, Options.ConsentCookieValue, cookieOptions);
_logger.ConsentGranted();
}
_hasConsent = true;
Expand All @@ -93,7 +92,7 @@ public void WithdrawConsent()
public string CreateConsentCookie()
{
var key = Options.ConsentCookie.Name;
var value = ConsentValue;
var value = Options.ConsentCookieValue;
var options = Options.ConsentCookie.Build(Context);

Debug.Assert(key != null);
Expand Down
59 changes: 59 additions & 0 deletions src/Security/CookiePolicy/test/CookieConsentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Net.Http.Headers;
Expand Down Expand Up @@ -638,6 +639,64 @@ public async Task CreateConsentCookieAppliesPolicy()
Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
}

[Fact]
public async Task CreateConsentCookieMatchesGrantConsentCookieWhenCookieValueIsCustom()
{
var httpContext = await RunTestAsync(options =>
{
options.CheckConsentNeeded = context => true;
options.ConsentCookieValue = "true";
},
requestContext => { },
context =>
{
var feature = context.Features.Get<ITrackingConsentFeature>();
Assert.True(feature.IsConsentNeeded);
Assert.False(feature.HasConsent);
Assert.False(feature.CanTrack);

feature.GrantConsent();

Assert.True(feature.IsConsentNeeded);
Assert.True(feature.HasConsent);
Assert.True(feature.CanTrack);

var cookie = feature.CreateConsentCookie();
context.Response.Headers["ManualCookie"] = cookie;

return Task.CompletedTask;
});

var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers.SetCookie);
Assert.Equal(1, cookies.Count);
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("true", consentCookie.Value);
Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);

cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
Assert.Equal(1, cookies.Count);
var manualCookie = cookies[0];
Assert.Equal(consentCookie.Name, manualCookie.Name);
Assert.Equal(consentCookie.Value, manualCookie.Value);
Assert.Equal(consentCookie.SameSite, manualCookie.SameSite);
Assert.NotNull(manualCookie.Expires); // Expires may not exactly match to the second.
}

[Theory]
[InlineData(null)]
[InlineData("")]
public void CreateCookiePolicyOptionsWithEmptyConsentCookieValueThrows(string value)
{
var options = new CookiePolicyOptions();

ExceptionAssert.ThrowsArgument(
() => options.ConsentCookieValue = value,
"value",
"Value cannot be null or empty string.");
}

private async Task<HttpContext> RunTestAsync(Action<CookiePolicyOptions> configureOptions, Action<HttpContext> configureRequest, RequestDelegate handleRequest)
{
var host = new HostBuilder()
Expand Down