#118 - Use common cookie header formatters.

This commit is contained in:
Chris Ross 2015-04-13 15:56:14 -07:00
parent a3b2d2c3eb
commit 99f3aa197f
16 changed files with 64 additions and 53 deletions

View File

@ -10,6 +10,7 @@ namespace CookieSample
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebEncoders();
services.AddDataProtection();
}

View File

@ -11,6 +11,7 @@ namespace CookieSessionSample
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebEncoders();
services.AddDataProtection();
}

View File

@ -12,6 +12,7 @@ namespace OpenIdConnectSample
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -18,6 +18,7 @@ namespace CookieSample
{
public void ConfigureServices(IServiceCollection services)
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.DataProtection;
using Microsoft.Framework.Internal;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.WebEncoders;
namespace Microsoft.AspNet.Authentication.Cookies
{
@ -20,6 +21,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
[NotNull] RequestDelegate next,
[NotNull] IDataProtectionProvider dataProtectionProvider,
[NotNull] ILoggerFactory loggerFactory,
[NotNull] IUrlEncoder urlEncoder,
[NotNull] IOptions<CookieAuthenticationOptions> options,
ConfigureOptions<CookieAuthenticationOptions> configureOptions)
: base(next, options, configureOptions)
@ -40,7 +42,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
}
if (Options.CookieManager == null)
{
Options.CookieManager = new ChunkingCookieManager();
Options.CookieManager = new ChunkingCookieManager(urlEncoder);
}
_logger = loggerFactory.CreateLogger(typeof(CookieAuthenticationMiddleware).FullName);

View File

@ -20,6 +20,7 @@ namespace Microsoft.Framework.DependencyInjection
public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] Action<CookieAuthenticationOptions> configure, string optionsName)
{
services.AddWebEncoders();
return services.Configure(configure, optionsName);
}
@ -30,6 +31,7 @@ namespace Microsoft.Framework.DependencyInjection
public static IServiceCollection ConfigureCookieAuthentication([NotNull] this IServiceCollection services, [NotNull] IConfiguration config, string optionsName)
{
services.AddWebEncoders();
return services.Configure<CookieAuthenticationOptions>(config, optionsName);
}
}

View File

@ -7,6 +7,8 @@ using System.Globalization;
using System.Linq;
using Microsoft.AspNet.Http;
using Microsoft.Framework.Internal;
using Microsoft.Framework.WebEncoders;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
{
@ -16,12 +18,13 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
/// </summary>
public class ChunkingCookieManager : ICookieManager
{
public ChunkingCookieManager()
public ChunkingCookieManager(IUrlEncoder urlEncoder)
{
// Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case.
// See http://browsercookielimits.x64.me/.
ChunkSize = 4090;
ThrowForPartialCookies = true;
Encoder = urlEncoder ?? UrlEncoder.Default;
}
/// <summary>
@ -38,6 +41,8 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
/// </summary>
public bool ThrowForPartialCookies { get; set; }
private IUrlEncoder Encoder { get; set; }
// Parse the "chunks:XX" to determine how many chunks there should be.
private static int ParseChunksCount(string value)
{
@ -119,22 +124,18 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
/// <param name="options"></param>
public void AppendResponseCookie([NotNull] HttpContext context, [NotNull] string key, string value, [NotNull] CookieOptions options)
{
var domainHasValue = !string.IsNullOrEmpty(options.Domain);
var pathHasValue = !string.IsNullOrEmpty(options.Path);
var expiresHasValue = options.Expires.HasValue;
var escapedKey = Encoder.UrlEncode(key);
var escapedKey = Uri.EscapeDataString(key);
var prefix = escapedKey + "=";
var template = new SetCookieHeaderValue(escapedKey)
{
Domain = options.Domain,
Expires = options.Expires,
HttpOnly = options.HttpOnly,
Path = options.Path,
Secure = options.Secure,
};
var suffix = string.Concat(
!domainHasValue ? null : "; domain=",
!domainHasValue ? null : options.Domain,
!pathHasValue ? null : "; path=",
!pathHasValue ? null : options.Path,
!expiresHasValue ? null : "; expires=",
!expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT",
!options.Secure ? null : "; secure",
!options.HttpOnly ? null : "; HttpOnly");
var templateLength = template.ToString().Length;
value = value ?? string.Empty;
var quoted = false;
@ -143,19 +144,16 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
quoted = true;
value = RemoveQuotes(value);
}
var escapedValue = Uri.EscapeDataString(value);
var escapedValue = Encoder.UrlEncode(value);
// Normal cookie
var responseHeaders = context.Response.Headers;
if (!ChunkSize.HasValue || ChunkSize.Value > prefix.Length + escapedValue.Length + suffix.Length + (quoted ? 2 : 0))
if (!ChunkSize.HasValue || ChunkSize.Value > templateLength + escapedValue.Length + (quoted ? 2 : 0))
{
var setCookieValue = string.Concat(
prefix,
quoted ? Quote(escapedValue) : escapedValue,
suffix);
responseHeaders.AppendValues(Constants.Headers.SetCookie, setCookieValue);
template.Value = quoted ? Quote(escapedValue) : escapedValue;
responseHeaders.AppendValues(Constants.Headers.SetCookie, template.ToString());
}
else if (ChunkSize.Value < prefix.Length + suffix.Length + (quoted ? 2 : 0) + 10)
else if (ChunkSize.Value < templateLength + (quoted ? 2 : 0) + 10)
{
// 10 is the minimum data we want to put in an individual cookie, including the cookie chunk identifier "CXX".
// No room for data, we can't chunk the options and name
@ -169,10 +167,11 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
// Set-Cookie: CookieNameC1="Segment1"; path=/
// Set-Cookie: CookieNameC2="Segment2"; path=/
// Set-Cookie: CookieNameC3="Segment3"; path=/
var dataSizePerCookie = ChunkSize.Value - prefix.Length - suffix.Length - (quoted ? 2 : 0) - 3; // Budget 3 chars for the chunkid.
var dataSizePerCookie = ChunkSize.Value - templateLength - (quoted ? 2 : 0) - 3; // Budget 3 chars for the chunkid.
var cookieChunkCount = (int)Math.Ceiling(escapedValue.Length * 1.0 / dataSizePerCookie);
responseHeaders.AppendValues(Constants.Headers.SetCookie, prefix + "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture) + suffix);
template.Value = "chunks:" + cookieChunkCount.ToString(CultureInfo.InvariantCulture);
responseHeaders.AppendValues(Constants.Headers.SetCookie, template.ToString());
var chunks = new string[cookieChunkCount];
var offset = 0;
@ -183,15 +182,9 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
var segment = escapedValue.Substring(offset, length);
offset += length;
chunks[chunkId - 1] = string.Concat(
escapedKey,
"C",
chunkId.ToString(CultureInfo.InvariantCulture),
"=",
quoted ? "\"" : string.Empty,
segment,
quoted ? "\"" : string.Empty,
suffix);
template.Name = escapedKey + "C" + chunkId.ToString(CultureInfo.InvariantCulture);
template.Value = quoted ? Quote(segment) : segment;
chunks[chunkId - 1] = template.ToString();
}
responseHeaders.AppendValues(Constants.Headers.SetCookie, chunks);
}
@ -206,7 +199,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
/// <param name="options"></param>
public void DeleteCookie([NotNull] HttpContext context, [NotNull] string key, [NotNull] CookieOptions options)
{
var escapedKey = Uri.EscapeDataString(key);
var escapedKey = Encoder.UrlEncode(key);
var keys = new List<string>();
keys.Add(escapedKey + "=");
@ -260,7 +253,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
for (int i = 1; i <= chunks; i++)
{
AppendResponseCookie(
context,
context,
key + "C" + i.ToString(CultureInfo.InvariantCulture),
string.Empty,
new CookieOptions()

View File

@ -4,6 +4,7 @@
"dependencies": {
"Microsoft.AspNet.Authentication": "1.0.0-*",
"Microsoft.Framework.NotNullAttribute.Internal": { "type": "build", "version": "1.0.0-*" },
"Microsoft.Framework.WebEncoders": "1.0.0-*",
"Newtonsoft.Json": "6.0.6"
},
"frameworks": {

View File

@ -573,6 +573,7 @@ namespace Microsoft.AspNet.Authentication.Cookies
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
if (claimsTransform != null)
{

View File

@ -16,7 +16,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
HttpContext context = new DefaultHttpContext();
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
new ChunkingCookieManager(null) { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
Assert.Equal(1, values.Count);
Assert.Equal("TestCookie=" + testString + "; path=/", values[0]);
@ -28,7 +28,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
HttpContext context = new DefaultHttpContext();
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
new ChunkingCookieManager() { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
new ChunkingCookieManager(null) { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
Assert.Equal(9, values.Count);
Assert.Equal(new[]
@ -51,7 +51,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
HttpContext context = new DefaultHttpContext();
string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"";
new ChunkingCookieManager() { ChunkSize = 32 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
new ChunkingCookieManager(null) { ChunkSize = 32 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
IList<string> values = context.Response.Headers.GetValues("Set-Cookie");
Assert.Equal(9, values.Count);
Assert.Equal(new[]
@ -82,7 +82,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
"TestCookieC6=JKLMNOPQR",
"TestCookieC7=STUVWXYZ");
string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie");
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Assert.Equal(testString, result);
}
@ -101,7 +101,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
"TestCookieC6=\"JKLMNOPQR\"",
"TestCookieC7=\"STUVWXYZ\"");
string result = new ChunkingCookieManager().GetRequestCookie(context, "TestCookie");
string result = new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie");
string testString = "\"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"";
Assert.Equal(testString, result);
}
@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
"TestCookieC6=JKLMNOPQR",
"TestCookieC7=STUVWXYZ");
Assert.Throws<FormatException>(() => new ChunkingCookieManager().GetRequestCookie(context, "TestCookie"));
Assert.Throws<FormatException>(() => new ChunkingCookieManager(null).GetRequestCookie(context, "TestCookie"));
}
[Fact]
@ -137,7 +137,7 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
"TestCookieC6=JKLMNOPQR",
"TestCookieC7=STUVWXYZ");
string result = new ChunkingCookieManager() { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie");
string result = new ChunkingCookieManager(null) { ThrowForPartialCookies = false }.GetRequestCookie(context, "TestCookie");
string testString = "chunks:7";
Assert.Equal(testString, result);
}
@ -148,19 +148,19 @@ namespace Microsoft.AspNet.Authentication.Cookies.Infrastructure
HttpContext context = new DefaultHttpContext();
context.Request.Headers.AppendValues("Cookie", "TestCookie=chunks:7");
new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" });
new ChunkingCookieManager(null).DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com" });
var cookies = context.Response.Headers.GetValues("Set-Cookie");
Assert.Equal(8, cookies.Count);
Assert.Equal(new[]
{
"TestCookie=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC1=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC2=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC3=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC4=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC5=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC6=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookieC7=; domain=foo.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT",
"TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
"TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/",
}, cookies);
}
}

View File

@ -29,6 +29,7 @@ namespace Microsoft.AspNet.Authentication.Facebook
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.ConfigureFacebookAuthentication(options =>
{
@ -74,6 +75,7 @@ namespace Microsoft.AspNet.Authentication.Facebook
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.ConfigureFacebookAuthentication(options =>
{

View File

@ -530,6 +530,7 @@ namespace Microsoft.AspNet.Authentication.Google
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -177,6 +177,7 @@ namespace Microsoft.AspNet.Authentication.Tests.MicrosoftAccount
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -436,6 +436,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
}
);
@ -454,6 +455,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
}
);

View File

@ -234,6 +234,7 @@ namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{

View File

@ -123,6 +123,7 @@ namespace Microsoft.AspNet.Authentication.Twitter
},
services =>
{
services.AddWebEncoders();
services.AddDataProtection();
services.Configure<ExternalAuthenticationOptions>(options =>
{