This commit is contained in:
Chris Ross (ASP.NET) 2017-11-17 15:08:30 -08:00
Родитель 45ab9485d3
Коммит f8b4f4c620
16 изменённых файлов: 1021 добавлений и 145 удалений

Просмотреть файл

@ -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);
}
}
}