diff --git a/Security.sln b/Security.sln
index f88d8576b3..543b3be264 100644
--- a/Security.sln
+++ b/Security.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.26730.10
+VisualStudioVersion = 15.0.27004.2002
MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}"
ProjectSection(SolutionItems) = preProject
@@ -72,6 +72,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
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}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookiePolicySample", "samples\CookiePolicySample\CookiePolicySample.csproj", "{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -462,6 +464,22 @@ Global
{58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x64.Build.0 = Release|Any CPU
{58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.ActiveCfg = Release|Any CPU
{58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x64.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Debug|x86.Build.0 = Debug|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x64.Build.0 = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.ActiveCfg = Release|Any CPU
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -491,6 +509,7 @@ Global
{3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
{51563775-C659-4907-9BAF-9995BAB87D01} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
{58194599-F07D-47A3-9DF2-E21A22C5EF9E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
diff --git a/samples/CookiePolicySample/CookiePolicySample.csproj b/samples/CookiePolicySample/CookiePolicySample.csproj
new file mode 100644
index 0000000000..fb2e7d9172
--- /dev/null
+++ b/samples/CookiePolicySample/CookiePolicySample.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net461;netcoreapp2.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/CookiePolicySample/Program.cs b/samples/CookiePolicySample/Program.cs
new file mode 100644
index 0000000000..12fc8ff287
--- /dev/null
+++ b/samples/CookiePolicySample/Program.cs
@@ -0,0 +1,26 @@
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CookiePolicySample
+{
+ public static class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .ConfigureLogging(factory =>
+ {
+ factory.AddConsole();
+ factory.AddFilter("Console", level => level >= LogLevel.Information);
+ })
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/samples/CookiePolicySample/Properties/launchSettings.json b/samples/CookiePolicySample/Properties/launchSettings.json
new file mode 100644
index 0000000000..38ca6fc37f
--- /dev/null
+++ b/samples/CookiePolicySample/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:1788/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "CookieSample": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:12345",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/CookiePolicySample/Startup.cs b/samples/CookiePolicySample/Startup.cs
new file mode 100644
index 0000000000..7ce9c2d2d2
--- /dev/null
+++ b/samples/CookiePolicySample/Startup.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+
+namespace CookiePolicySample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
+ .AddCookie();
+ services.Configure(options =>
+ {
+ options.CheckConsentNeeded = context => context.Request.PathBase.Equals("/NeedsConsent");
+
+ options.OnAppendCookie = context => { };
+ });
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseCookiePolicy();
+ app.UseAuthentication();
+
+ app.Map("/NeedsConsent", NestedApp);
+ app.Map("/NeedsNoConsent", NestedApp);
+ NestedApp(app);
+ }
+
+ private void NestedApp(IApplicationBuilder app)
+ {
+ app.Run(async context =>
+ {
+ var path = context.Request.Path;
+ switch (path)
+ {
+ case "/Login":
+ var user = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "bob") },
+ CookieAuthenticationDefaults.AuthenticationScheme));
+ await context.SignInAsync(user);
+ break;
+ case "/Logout":
+ await context.SignOutAsync();
+ break;
+ case "/CreateTempCookie":
+ context.Response.Cookies.Append("Temp", "1");
+ break;
+ case "/RemoveTempCookie":
+ context.Response.Cookies.Delete("Temp");
+ break;
+ case "/GrantConsent":
+ context.Features.Get().GrantConsent();
+ break;
+ case "/WithdrawConsent":
+ context.Features.Get().WithdrawConsent();
+ break;
+ }
+
+ // TODO: Debug log when cookie is suppressed
+
+ await HomePage(context);
+ });
+ }
+
+ private async Task HomePage(HttpContext context)
+ {
+ var response = context.Response;
+ var cookies = context.Request.Cookies;
+ response.ContentType = "text/html";
+ await response.WriteAsync("\r\n");
+
+ await response.WriteAsync($"Home
\r\n");
+ await response.WriteAsync($"Login
\r\n");
+ await response.WriteAsync($"Logout
\r\n");
+ await response.WriteAsync($"Create Temp Cookie
\r\n");
+ await response.WriteAsync($"Remove Temp Cookie
\r\n");
+ await response.WriteAsync($"Grant Consent
\r\n");
+ await response.WriteAsync($"Withdraw Consent
\r\n");
+ await response.WriteAsync("
\r\n");
+ await response.WriteAsync($"Needs Consent
\r\n");
+ await response.WriteAsync($"Needs No Consent
\r\n");
+ await response.WriteAsync("
\r\n");
+
+ var feature = context.Features.Get();
+ await response.WriteAsync($"Consent:
\r\n");
+ await response.WriteAsync($" - IsNeeded: {feature.IsConsentNeeded}
\r\n");
+ await response.WriteAsync($" - Has: {feature.HasConsent}
\r\n");
+ await response.WriteAsync($" - Can Track: {feature.CanTrack}
\r\n");
+ await response.WriteAsync("
\r\n");
+
+ await response.WriteAsync($"{cookies.Count} Request Cookies:
\r\n");
+ foreach (var cookie in cookies)
+ {
+ await response.WriteAsync($" - {cookie.Key} = {cookie.Value}
\r\n");
+ }
+ await response.WriteAsync("
\r\n");
+
+ var responseCookies = response.Headers[HeaderNames.SetCookie];
+ await response.WriteAsync($"{responseCookies.Count} Response Cookies:
\r\n");
+ foreach (var cookie in responseCookies)
+ {
+ await response.WriteAsync($" - {cookie}
\r\n");
+ }
+
+ await response.WriteAsync("");
+ }
+ }
+}
diff --git a/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs b/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs
index 7217e70d4f..42cc4e2f0f 100644
--- a/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs
+++ b/shared/Microsoft.AspNetCore.ChunkingCookieManager.Sources/ChunkingCookieManager.cs
@@ -285,6 +285,7 @@ namespace Microsoft.AspNetCore.Internal
Path = options.Path,
Domain = options.Domain,
SameSite = options.SameSite,
+ IsEssential = options.IsEssential,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
@@ -299,6 +300,7 @@ namespace Microsoft.AspNetCore.Internal
Path = options.Path,
Domain = options.Domain,
SameSite = options.SameSite,
+ IsEssential = options.IsEssential,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
diff --git a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs
index 04c71ed1ef..35017f9c4d 100644
--- a/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs
+++ b/src/Microsoft.AspNetCore.Authentication.Cookies/CookieAuthenticationOptions.cs
@@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
SameSite = SameSiteMode.Lax,
HttpOnly = true,
SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
};
///
diff --git a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs
index a40d374356..cbf6e8eab6 100644
--- a/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs
+++ b/src/Microsoft.AspNetCore.Authentication.OpenIdConnect/OpenIdConnectOptions.cs
@@ -74,6 +74,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
HttpOnly = true,
SameSite = SameSiteMode.None,
SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
};
}
diff --git a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs
index 86919d0925..03396807ee 100644
--- a/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs
+++ b/src/Microsoft.AspNetCore.Authentication.Twitter/TwitterOptions.cs
@@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
SecurePolicy = CookieSecurePolicy.SameAsRequest,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
+ IsEssential = true,
};
}
diff --git a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs
index daba1890fb..1bd3b210e5 100644
--- a/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs
+++ b/src/Microsoft.AspNetCore.Authentication/RemoteAuthenticationOptions.cs
@@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Authentication
HttpOnly = true,
SameSite = SameSiteMode.None,
SecurePolicy = CookieSecurePolicy.SameAsRequest,
+ IsEssential = true,
};
}
diff --git a/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs b/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs
index 1b13251f73..bbb4899c04 100644
--- a/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs
+++ b/src/Microsoft.AspNetCore.CookiePolicy/AppendCookieContext.cs
@@ -19,5 +19,8 @@ namespace Microsoft.AspNetCore.CookiePolicy
public CookieOptions CookieOptions { get; }
public string CookieName { get; set; }
public string CookieValue { get; set; }
+ public bool IsConsentNeeded { get; internal set; }
+ public bool HasConsent { get; internal set; }
+ public bool IssueCookie { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs
index 92adac9677..b99fed2c3d 100644
--- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs
+++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyMiddleware.cs
@@ -1,7 +1,6 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
@@ -27,157 +26,21 @@ namespace Microsoft.AspNetCore.CookiePolicy
public Task Invoke(HttpContext context)
{
var feature = context.Features.Get() ?? new ResponseCookiesFeature(context.Features);
- context.Features.Set(new CookiesWrapperFeature(context, Options, feature));
+ var wrapper = new ResponseCookiesWrapper(context, Options, feature);
+ context.Features.Set(new CookiesWrapperFeature(wrapper));
+ context.Features.Set(wrapper);
+
return _next(context);
}
private class CookiesWrapperFeature : IResponseCookiesFeature
{
- public CookiesWrapperFeature(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature)
+ public CookiesWrapperFeature(ResponseCookiesWrapper wrapper)
{
- Wrapper = new CookiesWrapper(context, options, feature);
+ Cookies = wrapper;
}
- public IResponseCookies Wrapper { get; }
-
- public IResponseCookies Cookies
- {
- get
- {
- return Wrapper;
- }
- }
- }
-
- private class CookiesWrapper : IResponseCookies
- {
- public CookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature)
- {
- Context = context;
- Feature = feature;
- Policy = options;
- }
-
- public HttpContext Context { get; }
-
- public IResponseCookiesFeature Feature { get; }
-
- public IResponseCookies Cookies
- {
- get
- {
- return Feature.Cookies;
- }
- }
-
- public CookiePolicyOptions Policy { get; }
-
- private bool PolicyRequiresCookieOptions()
- {
- return Policy.MinimumSameSitePolicy != SameSiteMode.None || Policy.HttpOnly != HttpOnlyPolicy.None || Policy.Secure != CookieSecurePolicy.None;
- }
-
- public void Append(string key, string value)
- {
- if (PolicyRequiresCookieOptions() || Policy.OnAppendCookie != null)
- {
- Append(key, value, new CookieOptions());
- }
- else
- {
- Cookies.Append(key, value);
- }
- }
-
- public void Append(string key, string value, CookieOptions options)
- {
- if (options == null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- ApplyPolicy(options);
- if (Policy.OnAppendCookie != null)
- {
- var context = new AppendCookieContext(Context, options, key, value);
- Policy.OnAppendCookie(context);
- key = context.CookieName;
- value = context.CookieValue;
- }
- Cookies.Append(key, value, options);
- }
-
- public void Delete(string key)
- {
- if (PolicyRequiresCookieOptions() || Policy.OnDeleteCookie != null)
- {
- Delete(key, new CookieOptions());
- }
- else
- {
- Cookies.Delete(key);
- }
- }
-
- public void Delete(string key, CookieOptions options)
- {
- if (options == null)
- {
- throw new ArgumentNullException(nameof(options));
- }
-
- ApplyPolicy(options);
- if (Policy.OnDeleteCookie != null)
- {
- var context = new DeleteCookieContext(Context, options, key);
- Policy.OnDeleteCookie(context);
- key = context.CookieName;
- }
- Cookies.Delete(key, options);
- }
-
- private void ApplyPolicy(CookieOptions options)
- {
- switch (Policy.Secure)
- {
- case CookieSecurePolicy.Always:
- options.Secure = true;
- break;
- case CookieSecurePolicy.SameAsRequest:
- options.Secure = Context.Request.IsHttps;
- break;
- case CookieSecurePolicy.None:
- break;
- default:
- throw new InvalidOperationException();
- }
- switch (Policy.MinimumSameSitePolicy)
- {
- case SameSiteMode.None:
- break;
- case SameSiteMode.Lax:
- if (options.SameSite == SameSiteMode.None)
- {
- options.SameSite = SameSiteMode.Lax;
- }
- break;
- case SameSiteMode.Strict:
- options.SameSite = SameSiteMode.Strict;
- break;
- default:
- throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Policy.MinimumSameSitePolicy.ToString()}");
- }
- switch (Policy.HttpOnly)
- {
- case HttpOnlyPolicy.Always:
- options.HttpOnly = true;
- break;
- case HttpOnlyPolicy.None:
- break;
- default:
- throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Policy.HttpOnly.ToString()}");
- }
- }
+ public IResponseCookies Cookies { get; }
}
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs
index 1e474bfe22..cc2deaa3aa 100644
--- a/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs
+++ b/src/Microsoft.AspNetCore.CookiePolicy/CookiePolicyOptions.cs
@@ -27,6 +27,18 @@ namespace Microsoft.AspNetCore.Builder
///
public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None;
+ public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder()
+ {
+ Name = ".AspNet.Consent",
+ Expiration = TimeSpan.FromDays(90),
+ IsEssential = true,
+ };
+
+ ///
+ /// Checks if consent policies should be evaluated on this request. The default is false.
+ ///
+ public Func CheckConsentNeeded { get; set; }
+
///
/// Called when a cookie is appended.
///
diff --git a/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs b/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs
index f0693bf71f..fd79ea8d4b 100644
--- a/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs
+++ b/src/Microsoft.AspNetCore.CookiePolicy/DeleteCookieContext.cs
@@ -17,5 +17,8 @@ namespace Microsoft.AspNetCore.CookiePolicy
public HttpContext Context { get; }
public CookieOptions CookieOptions { get; }
public string CookieName { get; set; }
+ public bool IsConsentNeeded { get; internal set; }
+ public bool HasConsent { get; internal set; }
+ public bool IssueCookie { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs
new file mode 100644
index 0000000000..fa68a3cbea
--- /dev/null
+++ b/src/Microsoft.AspNetCore.CookiePolicy/ResponseCookiesWrapper.cs
@@ -0,0 +1,220 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+
+namespace Microsoft.AspNetCore.CookiePolicy
+{
+ internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
+ {
+ private const string ConsentValue = "yes";
+
+ private bool? _isConsentNeeded;
+ private bool? _hasConsent;
+
+ public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature)
+ {
+ Context = context;
+ Feature = feature;
+ Options = options;
+ }
+
+ private HttpContext Context { get; }
+
+ private IResponseCookiesFeature Feature { get; }
+
+ private IResponseCookies Cookies => Feature.Cookies;
+
+ private CookiePolicyOptions Options { get; }
+
+ public bool IsConsentNeeded
+ {
+ get
+ {
+ if (!_isConsentNeeded.HasValue)
+ {
+ _isConsentNeeded = Options.CheckConsentNeeded == null ? false
+ : Options.CheckConsentNeeded(Context);
+ }
+
+ return _isConsentNeeded.Value;
+ }
+ }
+
+ public bool HasConsent
+ {
+ get
+ {
+ if (!_hasConsent.HasValue)
+ {
+ var cookie = Context.Request.Cookies[Options.ConsentCookie.Name];
+ _hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
+ }
+
+ return _hasConsent.Value;
+ }
+ }
+
+ public bool CanTrack => !IsConsentNeeded || HasConsent;
+
+ public void GrantConsent()
+ {
+ if (!HasConsent && !Context.Response.HasStarted)
+ {
+ 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);
+ }
+ _hasConsent = true;
+ }
+
+ public void WithdrawConsent()
+ {
+ if (HasConsent && !Context.Response.HasStarted)
+ {
+ 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.
+ Delete(Options.ConsentCookie.Name, cookieOptions);
+ }
+ _hasConsent = false;
+ }
+
+ private bool CheckPolicyRequired()
+ {
+ return !CanTrack
+ || Options.MinimumSameSitePolicy != SameSiteMode.None
+ || Options.HttpOnly != HttpOnlyPolicy.None
+ || Options.Secure != CookieSecurePolicy.None;
+ }
+
+ public void Append(string key, string value)
+ {
+ if (CheckPolicyRequired() || Options.OnAppendCookie != null)
+ {
+ Append(key, value, new CookieOptions());
+ }
+ else
+ {
+ Cookies.Append(key, value);
+ }
+ }
+
+ public void Append(string key, string value, CookieOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ var issueCookie = CanTrack || options.IsEssential;
+ ApplyPolicy(options);
+ if (Options.OnAppendCookie != null)
+ {
+ var context = new AppendCookieContext(Context, options, key, value)
+ {
+ IsConsentNeeded = IsConsentNeeded,
+ HasConsent = HasConsent,
+ IssueCookie = issueCookie,
+ };
+ Options.OnAppendCookie(context);
+
+ key = context.CookieName;
+ value = context.CookieValue;
+ issueCookie = context.IssueCookie;
+ }
+
+ if (issueCookie)
+ {
+ Cookies.Append(key, value, options);
+ }
+ }
+
+ public void Delete(string key)
+ {
+ if (CheckPolicyRequired() || Options.OnDeleteCookie != null)
+ {
+ Delete(key, new CookieOptions());
+ }
+ else
+ {
+ Cookies.Delete(key);
+ }
+ }
+
+ public void Delete(string key, CookieOptions options)
+ {
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+
+ // Assume you can always delete cookies unless directly overridden in the user event.
+ var issueCookie = true;
+ ApplyPolicy(options);
+ if (Options.OnDeleteCookie != null)
+ {
+ var context = new DeleteCookieContext(Context, options, key)
+ {
+ IsConsentNeeded = IsConsentNeeded,
+ HasConsent = HasConsent,
+ IssueCookie = issueCookie,
+ };
+ Options.OnDeleteCookie(context);
+
+ key = context.CookieName;
+ issueCookie = context.IssueCookie;
+ }
+
+ if (issueCookie)
+ {
+ Cookies.Delete(key, options);
+ }
+ }
+
+ private void ApplyPolicy(CookieOptions options)
+ {
+ switch (Options.Secure)
+ {
+ case CookieSecurePolicy.Always:
+ options.Secure = true;
+ break;
+ case CookieSecurePolicy.SameAsRequest:
+ options.Secure = Context.Request.IsHttps;
+ break;
+ case CookieSecurePolicy.None:
+ break;
+ default:
+ throw new InvalidOperationException();
+ }
+ switch (Options.MinimumSameSitePolicy)
+ {
+ case SameSiteMode.None:
+ break;
+ case SameSiteMode.Lax:
+ if (options.SameSite == SameSiteMode.None)
+ {
+ options.SameSite = SameSiteMode.Lax;
+ }
+ break;
+ case SameSiteMode.Strict:
+ options.SameSite = SameSiteMode.Strict;
+ break;
+ default:
+ throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
+ }
+ switch (Options.HttpOnly)
+ {
+ case HttpOnlyPolicy.Always:
+ options.HttpOnly = true;
+ break;
+ case HttpOnlyPolicy.None:
+ break;
+ default:
+ throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Options.HttpOnly.ToString()}");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs
new file mode 100644
index 0000000000..4e62d54a26
--- /dev/null
+++ b/test/Microsoft.AspNetCore.CookiePolicy.Test/CookieConsentTests.cs
@@ -0,0 +1,561 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using Xunit;
+
+namespace Microsoft.AspNetCore.CookiePolicy.Test
+{
+ public class CookieConsentTests
+ {
+ [Fact]
+ public async Task ConsentChecksOffByDefault()
+ {
+ var httpContext = await RunTestAsync(options => { }, requestContext => { }, context =>
+ {
+ var feature = context.Features.Get();
+ Assert.False(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task ConsentEnabledForTemplateScenario()
+ {
+ 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);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NonEssentialCookiesWithOptionsExcluded()
+ {
+ 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);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NonEssentialCookiesCanBeAllowedViaOnAppendCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnAppendCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.False(context.HasConsent);
+ Assert.False(context.IssueCookie);
+ context.IssueCookie = true;
+ };
+ },
+ requestContext => { }, context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = false });
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task NeedsConsentDoesNotPreventEssentialCookies()
+ {
+ 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);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task EssentialCookiesCanBeExcludedByOnAppendCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnAppendCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.True(context.HasConsent);
+ Assert.True(context.IssueCookie);
+ context.IssueCookie = false;
+ };
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value", new CookieOptions() { IsEssential = true });
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task HasConsentReadsRequestCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task HasConsentIgnoresInvalidRequestCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=IAmATeapot";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task GrantConsentSetsCookie()
+ {
+ 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);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(2, 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);
+ var testCookie = cookies[1];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("Value", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.Null(testCookie.Expires);
+ }
+
+ [Fact]
+ public async Task GrantConsentAppliesPolicyToConsentCookie()
+ {
+ 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);
+
+ 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);
+ }
+
+ [Fact]
+ public async Task GrantConsentWhenAlreadyHasItDoesNotSetCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ Assert.Equal("Test=Value; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task GrantConsentAfterResponseStartsSetsHasConsentButDoesNotSetCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext => { },
+ async context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ await context.Response.WriteAsync("Started.");
+
+ feature.GrantConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ Assert.Throws(() => context.Response.Cookies.Append("Test", "Value"));
+
+ await context.Response.WriteAsync("Granted.");
+ });
+
+ var reader = new StreamReader(httpContext.Response.Body);
+ Assert.Equal("Started.Granted.", await reader.ReadToEndAsync());
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentWhenNotHasConsentNoOps()
+ {
+ 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.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value");
+ return Task.CompletedTask;
+ });
+
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentDeletesCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value1");
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ context.Response.Cookies.Append("Test", "Value2");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(2, cookies.Count);
+ var testCookie = cookies[0];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("Value1", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.Null(testCookie.Expires);
+ var consentCookie = cookies[1];
+ Assert.Equal(".AspNet.Consent", consentCookie.Name);
+ Assert.Equal("", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentAppliesPolicyToDeleteCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.MinimumSameSitePolicy = Http.SameSiteMode.Strict;
+ options.OnDeleteCookie = context =>
+ {
+ Assert.Equal(".AspNet.Consent", context.CookieName);
+ context.CookieName += "1";
+ };
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ 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("", consentCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Strict, consentCookie.SameSite);
+ Assert.NotNull(consentCookie.Expires);
+ }
+
+ [Fact]
+ public async Task WithdrawConsentAfterResponseHasStartedDoesNotDeleteCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ },
+ requestContext =>
+ {
+ requestContext.Request.Headers[HeaderNames.Cookie] = ".AspNet.Consent=yes";
+ },
+ async context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.True(feature.HasConsent);
+ Assert.True(feature.CanTrack);
+ context.Response.Cookies.Append("Test", "Value1");
+
+ await context.Response.WriteAsync("Started.");
+
+ feature.WithdrawConsent();
+
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+
+ // Doesn't throw the normal InvalidOperationException because the cookie is never written
+ context.Response.Cookies.Append("Test", "Value2");
+
+ await context.Response.WriteAsync("Withdrawn.");
+ });
+
+ var reader = new StreamReader(httpContext.Response.Body);
+ Assert.Equal("Started.Withdrawn.", await reader.ReadToEndAsync());
+ Assert.Equal("Test=Value1; path=/; samesite=lax", httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ [Fact]
+ public async Task DeleteCookieDoesNotRequireConsent()
+ {
+ 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);
+ context.Response.Cookies.Delete("Test");
+ return Task.CompletedTask;
+ });
+
+ var cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ Assert.Equal(1, cookies.Count);
+ var testCookie = cookies[0];
+ Assert.Equal("Test", testCookie.Name);
+ Assert.Equal("", testCookie.Value);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Lax, testCookie.SameSite);
+ Assert.NotNull(testCookie.Expires);
+ }
+
+ [Fact]
+ public async Task OnDeleteCookieCanSuppressCookie()
+ {
+ var httpContext = await RunTestAsync(options =>
+ {
+ options.CheckConsentNeeded = context => true;
+ options.OnDeleteCookie = context =>
+ {
+ Assert.True(context.IsConsentNeeded);
+ Assert.False(context.HasConsent);
+ Assert.True(context.IssueCookie);
+ context.IssueCookie = false;
+ };
+ },
+ requestContext => { },
+ context =>
+ {
+ var feature = context.Features.Get();
+ Assert.True(feature.IsConsentNeeded);
+ Assert.False(feature.HasConsent);
+ Assert.False(feature.CanTrack);
+ context.Response.Cookies.Delete("Test");
+ return Task.CompletedTask;
+ });
+
+ Assert.Empty(httpContext.Response.Headers[HeaderNames.SetCookie]);
+ }
+
+ private Task RunTestAsync(Action configureOptions, Action configureRequest, RequestDelegate handleRequest)
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(services =>
+ {
+ services.Configure(configureOptions);
+ })
+ .Configure(app =>
+ {
+ app.UseCookiePolicy();
+ app.Run(handleRequest);
+ });
+ var server = new TestServer(builder);
+ return server.SendAsync(configureRequest);
+ }
+ }
+}
\ No newline at end of file