зеркало из https://github.com/aspnet/Security.git
Add consent to CookiePolicy #1561
This commit is contained in:
Родитель
45ab9485d3
Коммит
f8b4f4c620
21
Security.sln
21
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}
|
||||
|
|
|
@ -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,
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
|
|||
SameSite = SameSiteMode.Lax,
|
||||
HttpOnly = true,
|
||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||
IsEssential = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -74,6 +74,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
|
|||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||
IsEssential = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
|
|||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Lax,
|
||||
IsEssential = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Authentication
|
|||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
SecurePolicy = CookieSecurePolicy.SameAsRequest,
|
||||
IsEssential = true,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,6 +27,18 @@ namespace Microsoft.AspNetCore.Builder
|
|||
/// </summary>
|
||||
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>
|
||||
/// Called when a cookie is appended.
|
||||
/// </summary>
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче