Add consent to CookiePolicy #1561
This commit is contained in:
parent
45ab9485d3
commit
f8b4f4c620
21
Security.sln
21
Security.sln
|
|
@ -1,6 +1,6 @@
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.26730.10
|
VisualStudioVersion = 15.0.27004.2002
|
||||||
MinimumVisualStudioVersion = 15.0.26730.03
|
MinimumVisualStudioVersion = 15.0.26730.03
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4D2B6A51-2F9F-44F5-8131-EA5CAC053652}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
|
@ -72,6 +72,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
EndProject
|
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}"
|
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
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CookiePolicySample", "samples\CookiePolicySample\CookiePolicySample.csproj", "{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{58194599-F07D-47A3-9DF2-E21A22C5EF9E}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -491,6 +509,7 @@ Global
|
||||||
{3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
|
{3A7AD414-EBDE-4F92-B307-4E8F19B6117E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
|
||||||
{51563775-C659-4907-9BAF-9995BAB87D01} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
|
{51563775-C659-4907-9BAF-9995BAB87D01} = {7BF11F3A-60B6-4796-B504-579C67FFBA34}
|
||||||
{58194599-F07D-47A3-9DF2-E21A22C5EF9E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
|
{58194599-F07D-47A3-9DF2-E21A22C5EF9E} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
|
||||||
|
{24A28F5D-E5A9-4CA8-B0D2-924A1F8BE14E} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
|
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.CookiePolicy\Microsoft.AspNetCore.CookiePolicy.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Authentication.Cookies\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(MicrosoftExtensionsLoggingConsolePackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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<Startup>()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
host.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<CookiePolicyOptions>(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<ITrackingConsentFeature>().GrantConsent();
|
||||||
|
break;
|
||||||
|
case "/WithdrawConsent":
|
||||||
|
context.Features.Get<ITrackingConsentFeature>().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("<html><body>\r\n");
|
||||||
|
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/\">Home</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/Login\">Login</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/Logout\">Logout</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/CreateTempCookie\">Create Temp Cookie</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/RemoveTempCookie\">Remove Temp Cookie</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/GrantConsent\">Grant Consent</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"{context.Request.PathBase}/WithdrawConsent\">Withdraw Consent</a><br>\r\n");
|
||||||
|
await response.WriteAsync("<br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"/NeedsConsent{context.Request.Path}\">Needs Consent</a><br>\r\n");
|
||||||
|
await response.WriteAsync($"<a href=\"/NeedsNoConsent{context.Request.Path}\">Needs No Consent</a><br>\r\n");
|
||||||
|
await response.WriteAsync("<br>\r\n");
|
||||||
|
|
||||||
|
var feature = context.Features.Get<ITrackingConsentFeature>();
|
||||||
|
await response.WriteAsync($"Consent: <br>\r\n");
|
||||||
|
await response.WriteAsync($" - IsNeeded: {feature.IsConsentNeeded} <br>\r\n");
|
||||||
|
await response.WriteAsync($" - Has: {feature.HasConsent} <br>\r\n");
|
||||||
|
await response.WriteAsync($" - Can Track: {feature.CanTrack} <br>\r\n");
|
||||||
|
await response.WriteAsync("<br>\r\n");
|
||||||
|
|
||||||
|
await response.WriteAsync($"{cookies.Count} Request Cookies:<br>\r\n");
|
||||||
|
foreach (var cookie in cookies)
|
||||||
|
{
|
||||||
|
await response.WriteAsync($" - {cookie.Key} = {cookie.Value} <br>\r\n");
|
||||||
|
}
|
||||||
|
await response.WriteAsync("<br>\r\n");
|
||||||
|
|
||||||
|
var responseCookies = response.Headers[HeaderNames.SetCookie];
|
||||||
|
await response.WriteAsync($"{responseCookies.Count} Response Cookies:<br>\r\n");
|
||||||
|
foreach (var cookie in responseCookies)
|
||||||
|
{
|
||||||
|
await response.WriteAsync($" - {cookie} <br>\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
await response.WriteAsync("</body></html>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -285,6 +285,7 @@ namespace Microsoft.AspNetCore.Internal
|
||||||
Path = options.Path,
|
Path = options.Path,
|
||||||
Domain = options.Domain,
|
Domain = options.Domain,
|
||||||
SameSite = options.SameSite,
|
SameSite = options.SameSite,
|
||||||
|
IsEssential = options.IsEssential,
|
||||||
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -299,6 +300,7 @@ namespace Microsoft.AspNetCore.Internal
|
||||||
Path = options.Path,
|
Path = options.Path,
|
||||||
Domain = options.Domain,
|
Domain = options.Domain,
|
||||||
SameSite = options.SameSite,
|
SameSite = options.SameSite,
|
||||||
|
IsEssential = options.IsEssential,
|
||||||
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
|
||||||
SameSite = SameSiteMode.Lax,
|
SameSite = SameSiteMode.Lax,
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||||
|
IsEssential = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
SameSite = SameSiteMode.None,
|
SameSite = SameSiteMode.None,
|
||||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||||
|
IsEssential = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
||||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
SameSite = SameSiteMode.Lax,
|
SameSite = SameSiteMode.Lax,
|
||||||
|
IsEssential = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Authentication
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
SameSite = SameSiteMode.None,
|
SameSite = SameSiteMode.None,
|
||||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||||
|
IsEssential = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,5 +19,8 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
public CookieOptions CookieOptions { get; }
|
public CookieOptions CookieOptions { get; }
|
||||||
public string CookieName { get; set; }
|
public string CookieName { get; set; }
|
||||||
public string CookieValue { get; set; }
|
public string CookieValue { get; set; }
|
||||||
|
public bool IsConsentNeeded { get; internal set; }
|
||||||
|
public bool HasConsent { get; internal set; }
|
||||||
|
public bool IssueCookie { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
@ -27,157 +26,21 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
public Task Invoke(HttpContext context)
|
public Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
var feature = context.Features.Get<IResponseCookiesFeature>() ?? new ResponseCookiesFeature(context.Features);
|
var feature = context.Features.Get<IResponseCookiesFeature>() ?? new ResponseCookiesFeature(context.Features);
|
||||||
context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(context, Options, feature));
|
var wrapper = new ResponseCookiesWrapper(context, Options, feature);
|
||||||
|
context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(wrapper));
|
||||||
|
context.Features.Set<ITrackingConsentFeature>(wrapper);
|
||||||
|
|
||||||
return _next(context);
|
return _next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CookiesWrapperFeature : IResponseCookiesFeature
|
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; }
|
||||||
|
|
||||||
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()}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -27,6 +27,18 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None;
|
public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None;
|
||||||
|
|
||||||
|
public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder()
|
||||||
|
{
|
||||||
|
Name = ".AspNet.Consent",
|
||||||
|
Expiration = TimeSpan.FromDays(90),
|
||||||
|
IsEssential = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if consent policies should be evaluated on this request. The default is false.
|
||||||
|
/// </summary>
|
||||||
|
public Func<HttpContext, bool> CheckConsentNeeded { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called when a cookie is appended.
|
/// Called when a cookie is appended.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,8 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
public HttpContext Context { get; }
|
public HttpContext Context { get; }
|
||||||
public CookieOptions CookieOptions { get; }
|
public CookieOptions CookieOptions { get; }
|
||||||
public string CookieName { get; set; }
|
public string CookieName { get; set; }
|
||||||
|
public bool IsConsentNeeded { get; internal set; }
|
||||||
|
public bool HasConsent { get; internal set; }
|
||||||
|
public bool IssueCookie { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
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<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);
|
||||||
|
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<InvalidOperationException>(() => 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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<ITrackingConsentFeature>();
|
||||||
|
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<HttpContext> RunTestAsync(Action<CookiePolicyOptions> configureOptions, Action<HttpContext> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue