diff --git a/Security.sln b/Security.sln index 543b3be264..f598f34eb1 100644 --- a/Security.sln +++ b/Security.sln @@ -68,6 +68,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build\Key.snk = build\Key.snk NuGet.config = NuGet.config build\repo.props = build\repo.props + build\sources.props = build\sources.props EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authorization.Policy", "src\Microsoft.AspNetCore.Authorization.Policy\Microsoft.AspNetCore.Authorization.Policy.csproj", "{58194599-F07D-47A3-9DF2-E21A22C5EF9E}" diff --git a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs index fa68a3cbea..e05cc9466f 100644 --- a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs +++ b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs @@ -82,6 +82,30 @@ namespace Microsoft.AspNetCore.CookiePolicy _hasConsent = false; } + // Note policy will be applied. We don't want to bypass policy because we want HttpOnly, Secure, etc. to apply. + public string CreateConsentCookie() + { + var key = Options.ConsentCookie.Name; + var value = ConsentValue; + var options = Options.ConsentCookie.Build(Context); + ApplyAppendPolicy(ref key, ref value, options); + + var setCookieHeaderValue = new Net.Http.Headers.SetCookieHeaderValue( + Uri.EscapeDataString(key), + Uri.EscapeDataString(value)) + { + Domain = options.Domain, + Path = options.Path, + Expires = options.Expires, + MaxAge = options.MaxAge, + Secure = options.Secure, + SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite, + HttpOnly = options.HttpOnly + }; + + return setCookieHeaderValue.ToString(); + } + private bool CheckPolicyRequired() { return !CanTrack @@ -109,6 +133,14 @@ namespace Microsoft.AspNetCore.CookiePolicy throw new ArgumentNullException(nameof(options)); } + if (ApplyAppendPolicy(ref key, ref value, options)) + { + Cookies.Append(key, value, options); + } + } + + private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options) + { var issueCookie = CanTrack || options.IsEssential; ApplyPolicy(options); if (Options.OnAppendCookie != null) @@ -126,10 +158,7 @@ namespace Microsoft.AspNetCore.CookiePolicy issueCookie = context.IssueCookie; } - if (issueCookie) - { - Cookies.Append(key, value, options); - } + return issueCookie; } public void Delete(string key) diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs index 4e62d54a26..3cd018e570 100644 --- a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs +++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs @@ -542,6 +542,91 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]); } + [Fact] + public async Task CreateConsentCookieMatchesGrantConsentCookie() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => 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[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent", consentCookie.Name); + Assert.Equal("yes", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + Assert.Equal(httpContext.Response.Headers[HeaderNames.SetCookie], httpContext.Response.Headers["ManualCookie"]); + } + + [Fact] + public async Task CreateConsentCookieAppliesPolicy() + { + var httpContext = await RunTestAsync(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = Http.SameSiteMode.Strict; + options.OnAppendCookie = context => + { + Assert.Equal(".AspNet.Consent", context.CookieName); + Assert.Equal("yes", context.CookieValue); + Assert.Equal(Http.SameSiteMode.Strict, context.CookieOptions.SameSite); + context.CookieName += "1"; + context.CookieValue += "1"; + }; + }, + 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[HeaderNames.SetCookie]); + Assert.Equal(1, cookies.Count); + var consentCookie = cookies[0]; + Assert.Equal(".AspNet.Consent1", consentCookie.Name); + Assert.Equal("yes1", consentCookie.Value); + Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite); + Assert.NotNull(consentCookie.Expires); + + Assert.Equal(httpContext.Response.Headers[HeaderNames.SetCookie], httpContext.Response.Headers["ManualCookie"]); + } + private Task RunTestAsync(Action configureOptions, Action configureRequest, RequestDelegate handleRequest) { var builder = new WebHostBuilder()