Add CookiePolicy logging #1588
This commit is contained in:
parent
21acbf06e8
commit
9839799645
|
|
@ -12,7 +12,7 @@ namespace CookiePolicySample
|
||||||
.ConfigureLogging(factory =>
|
.ConfigureLogging(factory =>
|
||||||
{
|
{
|
||||||
factory.AddConsole();
|
factory.AddConsole();
|
||||||
factory.AddFilter("Console", level => level >= LogLevel.Information);
|
factory.AddFilter("Microsoft", LogLevel.Trace);
|
||||||
})
|
})
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
// 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;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.CookiePolicy
|
namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
|
|
@ -12,13 +15,20 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
public class CookiePolicyMiddleware
|
public class CookiePolicyMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public CookiePolicyMiddleware(
|
public CookiePolicyMiddleware(RequestDelegate next, IOptions<CookiePolicyOptions> options, ILoggerFactory factory)
|
||||||
RequestDelegate next,
|
{
|
||||||
IOptions<CookiePolicyOptions> options)
|
Options = options.Value;
|
||||||
|
_next = next ?? throw new ArgumentNullException(nameof(next));
|
||||||
|
_logger = factory.CreateLogger<CookiePolicyMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CookiePolicyMiddleware(RequestDelegate next, IOptions<CookiePolicyOptions> options)
|
||||||
{
|
{
|
||||||
Options = options.Value;
|
Options = options.Value;
|
||||||
_next = next;
|
_next = next;
|
||||||
|
_logger = NullLogger.Instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CookiePolicyOptions Options { get; set; }
|
public CookiePolicyOptions Options { get; set; }
|
||||||
|
|
@ -26,7 +36,7 @@ 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);
|
||||||
var wrapper = new ResponseCookiesWrapper(context, Options, feature);
|
var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger);
|
||||||
context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(wrapper));
|
context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(wrapper));
|
||||||
context.Features.Set<ITrackingConsentFeature>(wrapper);
|
context.Features.Set<ITrackingConsentFeature>(wrapper);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Logging
|
||||||
|
{
|
||||||
|
internal static class LoggingExtensions
|
||||||
|
{
|
||||||
|
private static Action<ILogger, bool, Exception> _needsConsent;
|
||||||
|
private static Action<ILogger, bool, Exception> _hasConsent;
|
||||||
|
private static Action<ILogger, Exception> _consentGranted;
|
||||||
|
private static Action<ILogger, Exception> _consentWithdrawn;
|
||||||
|
private static Action<ILogger, string, Exception> _cookieSuppressed;
|
||||||
|
private static Action<ILogger, string, Exception> _deleteCookieSuppressed;
|
||||||
|
private static Action<ILogger, string, Exception> _upgradedToSecure;
|
||||||
|
private static Action<ILogger, string, string, Exception> _upgradedSameSite;
|
||||||
|
private static Action<ILogger, string, Exception> _upgradedToHttpOnly;
|
||||||
|
|
||||||
|
static LoggingExtensions()
|
||||||
|
{
|
||||||
|
_needsConsent = LoggerMessage.Define<bool>(
|
||||||
|
eventId: 1,
|
||||||
|
logLevel: LogLevel.Trace,
|
||||||
|
formatString: "Needs consent: {needsConsent}.");
|
||||||
|
_hasConsent = LoggerMessage.Define<bool>(
|
||||||
|
eventId: 2,
|
||||||
|
logLevel: LogLevel.Trace,
|
||||||
|
formatString: "Has consent: {hasConsent}.");
|
||||||
|
_consentGranted = LoggerMessage.Define(
|
||||||
|
eventId: 3,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Consent granted.");
|
||||||
|
_consentWithdrawn = LoggerMessage.Define(
|
||||||
|
eventId: 4,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Consent withdrawn.");
|
||||||
|
_cookieSuppressed = LoggerMessage.Define<string>(
|
||||||
|
eventId: 5,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Cookie '{key}' suppressed due to consent policy.");
|
||||||
|
_deleteCookieSuppressed = LoggerMessage.Define<string>(
|
||||||
|
eventId: 6,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Delete cookie '{key}' suppressed due to developer policy.");
|
||||||
|
_upgradedToSecure = LoggerMessage.Define<string>(
|
||||||
|
eventId: 7,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Cookie '{key}' upgraded to 'secure'.");
|
||||||
|
_upgradedSameSite = LoggerMessage.Define<string, string>(
|
||||||
|
eventId: 8,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Cookie '{key}' same site mode upgraded to '{mode}'.");
|
||||||
|
_upgradedToHttpOnly = LoggerMessage.Define<string>(
|
||||||
|
eventId: 9,
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
formatString: "Cookie '{key}' upgraded to 'httponly'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void NeedsConsent(this ILogger logger, bool needsConsent)
|
||||||
|
{
|
||||||
|
_needsConsent(logger, needsConsent, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HasConsent(this ILogger logger, bool hasConsent)
|
||||||
|
{
|
||||||
|
_hasConsent(logger, hasConsent, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ConsentGranted(this ILogger logger)
|
||||||
|
{
|
||||||
|
_consentGranted(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ConsentWithdrawn(this ILogger logger)
|
||||||
|
{
|
||||||
|
_consentWithdrawn(logger, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CookieSuppressed(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_cookieSuppressed(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void DeleteCookieSuppressed(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_deleteCookieSuppressed(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CookieUpgradedToSecure(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_upgradedToSecure(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CookieSameSiteUpgraded(this ILogger logger, string key, string mode)
|
||||||
|
{
|
||||||
|
_upgradedSameSite(logger, key, mode, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CookieUpgradedToHttpOnly(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_upgradedToHttpOnly(logger, key, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,23 @@ using System;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.CookiePolicy
|
namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
{
|
{
|
||||||
internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
|
internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
|
||||||
{
|
{
|
||||||
private const string ConsentValue = "yes";
|
private const string ConsentValue = "yes";
|
||||||
|
private readonly ILogger _logger;
|
||||||
private bool? _isConsentNeeded;
|
private bool? _isConsentNeeded;
|
||||||
private bool? _hasConsent;
|
private bool? _hasConsent;
|
||||||
|
|
||||||
public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature)
|
public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger)
|
||||||
{
|
{
|
||||||
Context = context;
|
Context = context;
|
||||||
Feature = feature;
|
Feature = feature;
|
||||||
Options = options;
|
Options = options;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpContext Context { get; }
|
private HttpContext Context { get; }
|
||||||
|
|
@ -38,6 +40,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
{
|
{
|
||||||
_isConsentNeeded = Options.CheckConsentNeeded == null ? false
|
_isConsentNeeded = Options.CheckConsentNeeded == null ? false
|
||||||
: Options.CheckConsentNeeded(Context);
|
: Options.CheckConsentNeeded(Context);
|
||||||
|
_logger.NeedsConsent(_isConsentNeeded.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _isConsentNeeded.Value;
|
return _isConsentNeeded.Value;
|
||||||
|
|
@ -52,6 +55,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
{
|
{
|
||||||
var cookie = Context.Request.Cookies[Options.ConsentCookie.Name];
|
var cookie = Context.Request.Cookies[Options.ConsentCookie.Name];
|
||||||
_hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
|
_hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
|
||||||
|
_logger.HasConsent(_hasConsent.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _hasConsent.Value;
|
return _hasConsent.Value;
|
||||||
|
|
@ -67,6 +71,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
var cookieOptions = Options.ConsentCookie.Build(Context);
|
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.
|
// 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);
|
Append(Options.ConsentCookie.Name, ConsentValue, cookieOptions);
|
||||||
|
_logger.ConsentGranted();
|
||||||
}
|
}
|
||||||
_hasConsent = true;
|
_hasConsent = true;
|
||||||
}
|
}
|
||||||
|
|
@ -78,6 +83,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
var cookieOptions = Options.ConsentCookie.Build(Context);
|
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.
|
// 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);
|
Delete(Options.ConsentCookie.Name, cookieOptions);
|
||||||
|
_logger.ConsentWithdrawn();
|
||||||
}
|
}
|
||||||
_hasConsent = false;
|
_hasConsent = false;
|
||||||
}
|
}
|
||||||
|
|
@ -137,12 +143,16 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
{
|
{
|
||||||
Cookies.Append(key, value, options);
|
Cookies.Append(key, value, options);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.CookieSuppressed(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options)
|
private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options)
|
||||||
{
|
{
|
||||||
var issueCookie = CanTrack || options.IsEssential;
|
var issueCookie = CanTrack || options.IsEssential;
|
||||||
ApplyPolicy(options);
|
ApplyPolicy(key, options);
|
||||||
if (Options.OnAppendCookie != null)
|
if (Options.OnAppendCookie != null)
|
||||||
{
|
{
|
||||||
var context = new AppendCookieContext(Context, options, key, value)
|
var context = new AppendCookieContext(Context, options, key, value)
|
||||||
|
|
@ -182,7 +192,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
|
|
||||||
// Assume you can always delete cookies unless directly overridden in the user event.
|
// Assume you can always delete cookies unless directly overridden in the user event.
|
||||||
var issueCookie = true;
|
var issueCookie = true;
|
||||||
ApplyPolicy(options);
|
ApplyPolicy(key, options);
|
||||||
if (Options.OnDeleteCookie != null)
|
if (Options.OnDeleteCookie != null)
|
||||||
{
|
{
|
||||||
var context = new DeleteCookieContext(Context, options, key)
|
var context = new DeleteCookieContext(Context, options, key)
|
||||||
|
|
@ -201,17 +211,30 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
{
|
{
|
||||||
Cookies.Delete(key, options);
|
Cookies.Delete(key, options);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.DeleteCookieSuppressed(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyPolicy(CookieOptions options)
|
private void ApplyPolicy(string key, CookieOptions options)
|
||||||
{
|
{
|
||||||
switch (Options.Secure)
|
switch (Options.Secure)
|
||||||
{
|
{
|
||||||
case CookieSecurePolicy.Always:
|
case CookieSecurePolicy.Always:
|
||||||
options.Secure = true;
|
if (!options.Secure)
|
||||||
|
{
|
||||||
|
options.Secure = true;
|
||||||
|
_logger.CookieUpgradedToSecure(key);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CookieSecurePolicy.SameAsRequest:
|
case CookieSecurePolicy.SameAsRequest:
|
||||||
options.Secure = Context.Request.IsHttps;
|
// Never downgrade a cookie
|
||||||
|
if (Context.Request.IsHttps && !options.Secure)
|
||||||
|
{
|
||||||
|
options.Secure = true;
|
||||||
|
_logger.CookieUpgradedToSecure(key);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case CookieSecurePolicy.None:
|
case CookieSecurePolicy.None:
|
||||||
break;
|
break;
|
||||||
|
|
@ -226,10 +249,15 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
if (options.SameSite == SameSiteMode.None)
|
if (options.SameSite == SameSiteMode.None)
|
||||||
{
|
{
|
||||||
options.SameSite = SameSiteMode.Lax;
|
options.SameSite = SameSiteMode.Lax;
|
||||||
|
_logger.CookieSameSiteUpgraded(key, "lax");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SameSiteMode.Strict:
|
case SameSiteMode.Strict:
|
||||||
options.SameSite = SameSiteMode.Strict;
|
if (options.SameSite != SameSiteMode.Strict)
|
||||||
|
{
|
||||||
|
options.SameSite = SameSiteMode.Strict;
|
||||||
|
_logger.CookieSameSiteUpgraded(key, "strict");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
|
throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
|
||||||
|
|
@ -237,7 +265,11 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
switch (Options.HttpOnly)
|
switch (Options.HttpOnly)
|
||||||
{
|
{
|
||||||
case HttpOnlyPolicy.Always:
|
case HttpOnlyPolicy.Always:
|
||||||
options.HttpOnly = true;
|
if (!options.HttpOnly)
|
||||||
|
{
|
||||||
|
options.HttpOnly = true;
|
||||||
|
_logger.CookieUpgradedToHttpOnly(key);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case HttpOnlyPolicy.None:
|
case HttpOnlyPolicy.None:
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
|
||||||
Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
|
Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
|
||||||
Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
|
Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
|
||||||
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
|
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
|
||||||
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
|
Assert.Equal("D=D; path=/; secure; samesite=lax", transaction.SetCookie[3]);
|
||||||
}),
|
}),
|
||||||
new RequestTest("https://example.com/secureSame",
|
new RequestTest("https://example.com/secureSame",
|
||||||
transaction =>
|
transaction =>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue