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()