diff --git a/src/Security/CookiePolicy/src/CookiePolicyOptions.cs b/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
index a02092a6964b..9dd0d3fba185 100644
--- a/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
+++ b/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
@@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Builder;
///
public class CookiePolicyOptions
{
+ private string _consentCookieValue = "yes";
+
///
/// Affects the cookie's same site attribute.
///
@@ -37,6 +39,24 @@ public class CookiePolicyOptions
IsEssential = true,
};
+ ///
+ /// Gets or sets the value for the cookie used to track if the user consented to the
+ /// cookie use policy.
+ ///
+ /// Defaults to yes.
+ public string ConsentCookieValue
+ {
+ get => _consentCookieValue;
+ set
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ throw new ArgumentException("Value cannot be null or empty string.", nameof(value));
+ }
+ _consentCookieValue = value;
+ }
+ }
+
///
/// Checks if consent policies should be evaluated on this request. The default is false.
///
diff --git a/src/Security/CookiePolicy/src/PublicAPI.Unshipped.txt b/src/Security/CookiePolicy/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..b37004541aaa 100644
--- a/src/Security/CookiePolicy/src/PublicAPI.Unshipped.txt
+++ b/src/Security/CookiePolicy/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Builder.CookiePolicyOptions.ConsentCookieValue.get -> string!
+Microsoft.AspNetCore.Builder.CookiePolicyOptions.ConsentCookieValue.set -> void
\ No newline at end of file
diff --git a/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs b/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
index 11a74ffa85b7..2de742c7b29a 100644
--- a/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
+++ b/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
@@ -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;
@@ -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);
}
@@ -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;
@@ -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);
diff --git a/src/Security/CookiePolicy/test/CookieConsentTests.cs b/src/Security/CookiePolicy/test/CookieConsentTests.cs
index 23144105605c..e217f18da910 100644
--- a/src/Security/CookiePolicy/test/CookieConsentTests.cs
+++ b/src/Security/CookiePolicy/test/CookieConsentTests.cs
@@ -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;
@@ -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();
+ 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 RunTestAsync(Action configureOptions, Action configureRequest, RequestDelegate handleRequest)
{
var host = new HostBuilder()