Add SameSitePolicy to CookiePolicyMiddleware

This commit is contained in:
John Luo 2017-05-18 20:12:41 -07:00
parent 2a4a7dd26a
commit 769da5fd87
11 changed files with 165 additions and 51 deletions

View File

@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.Internal
/// <summary>
/// The default maximum size of characters in a cookie to send back to the client.
/// </summary>
public const int DefaultChunkSize = 4070;
public const int DefaultChunkSize = 4050;
private const string ChunkKeySuffix = "C";
private const string ChunkCountPrefix = "chunks-";
@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.Internal
{
// Lowest common denominator. Safari has the lowest known limit (4093), and we leave little extra just in case.
// See http://browsercookielimits.x64.me/.
// Leave at least 20 in case CookiePolicy tries to add 'secure' and/or 'httponly'.
// Leave at least 40 in case CookiePolicy tries to add 'secure', 'samesite=strict' and/or 'httponly'.
ChunkSize = DefaultChunkSize;
ThrowForPartialCookies = true;
}
@ -166,6 +166,7 @@ namespace Microsoft.AspNetCore.Internal
{
Domain = options.Domain,
Expires = options.Expires,
SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
HttpOnly = options.HttpOnly,
Path = options.Path,
Secure = options.Secure,
@ -284,6 +285,7 @@ namespace Microsoft.AspNetCore.Internal
{
Path = options.Path,
Domain = options.Domain,
SameSite = options.SameSite,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
@ -297,6 +299,7 @@ namespace Microsoft.AspNetCore.Internal
{
Path = options.Path,
Domain = options.Domain,
SameSite = options.SameSite,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}

View File

@ -179,6 +179,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
var cookieOptions = new CookieOptions
{
Domain = Options.CookieDomain,
SameSite = Options.CookieSameSite,
HttpOnly = Options.CookieHttpOnly,
Path = Options.CookiePath ?? (OriginalPathBase.HasValue ? OriginalPathBase.ToString() : "/"),
};

View File

@ -4,7 +4,6 @@
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Authentication.Cookies
{
@ -23,6 +22,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
ExpireTimeSpan = TimeSpan.FromDays(14);
SlidingExpiration = true;
CookieSameSite = SameSiteMode.Strict;
CookieHttpOnly = true;
CookieSecure = CookieSecurePolicy.SameAsRequest;
Events = new CookieAuthenticationEvents();
@ -57,6 +57,12 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
/// </summary>
public string CookiePath { get; set; }
/// <summary>
/// Determines if the browser should allow the cookie to be attached to same-site or cross-site requests. The
/// default is Strict, which means the cookie is only allowed to be attached to same-site requests.
/// </summary>
public SameSiteMode CookieSameSite { get; set; }
/// <summary>
/// Determines if the browser should allow the cookie to be accessed by client-side javascript. The
/// default is true, which means the cookie will only be passed to http requests and is not made available

View File

@ -899,6 +899,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
new CookieOptions
{
HttpOnly = true,
SameSite = Http.SameSiteMode.Lax,
Secure = Request.IsHttps,
Expires = Clock.UtcNow.Add(Options.ProtocolValidator.NonceLifetime)
});
@ -930,6 +931,7 @@ namespace Microsoft.AspNetCore.Authentication.OpenIdConnect
var cookieOptions = new CookieOptions
{
HttpOnly = true,
SameSite = Http.SameSiteMode.Lax,
Secure = Request.IsHttps
};

View File

@ -83,6 +83,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
var cookieOptions = new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = Request.IsHttps
};
@ -160,6 +161,7 @@ namespace Microsoft.AspNetCore.Authentication.Twitter
var cookieOptions = new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = Request.IsHttps,
Expires = Clock.UtcNow.Add(Options.RemoteAuthenticationTimeout),
};

View File

@ -203,6 +203,7 @@ namespace Microsoft.AspNetCore.Authentication
var cookieOptions = new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = Request.IsHttps,
Expires = Clock.UtcNow.Add(Options.RemoteAuthenticationTimeout),
};
@ -242,6 +243,7 @@ namespace Microsoft.AspNetCore.Authentication
var cookieOptions = new CookieOptions
{
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Secure = Request.IsHttps
};
Response.Cookies.Delete(cookieName, cookieOptions);

View File

@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
private bool PolicyRequiresCookieOptions()
{
return Policy.HttpOnly != HttpOnlyPolicy.None || Policy.Secure != CookieSecurePolicy.None;
return Policy.MinimumSameSitePolicy != SameSiteMode.None || Policy.HttpOnly != HttpOnlyPolicy.None || Policy.Secure != CookieSecurePolicy.None;
}
public void Append(string key, string value)
@ -151,6 +151,22 @@ namespace Microsoft.AspNetCore.CookiePolicy
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:
@ -159,7 +175,7 @@ namespace Microsoft.AspNetCore.CookiePolicy
case HttpOnlyPolicy.None:
break;
default:
throw new InvalidOperationException();
throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Policy.HttpOnly.ToString()}");
}
}
}

View File

@ -12,6 +12,11 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public class CookiePolicyOptions
{
/// <summary>
/// Affects the cookie's same site attribute.
/// </summary>
public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.Strict;
/// <summary>
/// Affects whether cookies must be HttpOnly.
/// </summary>

View File

@ -136,6 +136,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
Assert.StartsWith("TestCookie=", setCookie);
Assert.Contains("; path=/", setCookie);
Assert.Contains("; httponly", setCookie);
Assert.Contains("; samesite=", setCookie);
Assert.DoesNotContain("; expires=", setCookie);
Assert.DoesNotContain("; domain=", setCookie);
Assert.DoesNotContain("; secure", setCookie);
@ -206,6 +207,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
o.CookiePath = "/foo";
o.CookieDomain = "another.com";
o.CookieSecure = CookieSecurePolicy.Always;
o.CookieSameSite = SameSiteMode.None;
o.CookieHttpOnly = true;
}, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));
@ -217,12 +219,14 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
Assert.Contains(" path=/foo", setCookie1);
Assert.Contains(" domain=another.com", setCookie1);
Assert.Contains(" secure", setCookie1);
Assert.DoesNotContain(" samesite", setCookie1);
Assert.Contains(" httponly", setCookie1);
var server2 = CreateServer(o =>
{
o.CookieName = "SecondCookie";
o.CookieSecure = CookieSecurePolicy.None;
o.CookieSameSite = SameSiteMode.Strict;
o.CookieHttpOnly = false;
}, SignInAsAlice, baseAddress: new Uri("http://example.com/base"));
@ -232,6 +236,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
Assert.Contains("SecondCookie=", setCookie2);
Assert.Contains(" path=/base", setCookie2);
Assert.Contains(" samesite=strict", setCookie2);
Assert.DoesNotContain(" domain=", setCookie2);
Assert.DoesNotContain(" secure", setCookie2);
Assert.DoesNotContain(" httponly", setCookie2);

View File

@ -18,7 +18,7 @@ namespace Microsoft.AspNetCore.Internal
new ChunkingCookieManager() { ChunkSize = null }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
var values = context.Response.Headers["Set-Cookie"];
Assert.Equal(1, values.Count);
Assert.Equal("TestCookie=" + testString + "; path=/", values[0]);
Assert.Equal("TestCookie=" + testString + "; path=/; samesite=lax", values[0]);
}
[Fact]
@ -27,20 +27,20 @@ namespace Microsoft.AspNetCore.Internal
HttpContext context = new DefaultHttpContext();
string testString = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
new ChunkingCookieManager() { ChunkSize = 30 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
new ChunkingCookieManager() { ChunkSize = 44 }.AppendResponseCookie(context, "TestCookie", testString, new CookieOptions());
var values = context.Response.Headers["Set-Cookie"];
Assert.Equal(9, values.Count);
Assert.Equal<string[]>(new[]
{
"TestCookie=chunks-8; path=/",
"TestCookieC1=abcdefgh; path=/",
"TestCookieC2=ijklmnop; path=/",
"TestCookieC3=qrstuvwx; path=/",
"TestCookieC4=yz012345; path=/",
"TestCookieC5=6789ABCD; path=/",
"TestCookieC6=EFGHIJKL; path=/",
"TestCookieC7=MNOPQRST; path=/",
"TestCookieC8=UVWXYZ; path=/",
"TestCookie=chunks-8; path=/; samesite=lax",
"TestCookieC1=abcdefgh; path=/; samesite=lax",
"TestCookieC2=ijklmnop; path=/; samesite=lax",
"TestCookieC3=qrstuvwx; path=/; samesite=lax",
"TestCookieC4=yz012345; path=/; samesite=lax",
"TestCookieC5=6789ABCD; path=/; samesite=lax",
"TestCookieC6=EFGHIJKL; path=/; samesite=lax",
"TestCookieC7=MNOPQRST; path=/; samesite=lax",
"TestCookieC8=UVWXYZ; path=/; samesite=lax",
}, values);
}
@ -116,14 +116,14 @@ namespace Microsoft.AspNetCore.Internal
Assert.Equal(8, cookies.Count);
Assert.Equal(new[]
{
"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=/",
"TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
"TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; samesite=lax",
}, cookies);
}
}

View File

@ -36,6 +36,15 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
context.Response.Cookies.Append("D", "D", new CookieOptions { HttpOnly = true });
return Task.FromResult(0);
};
private RequestDelegate SameSiteCookieAppends = context =>
{
context.Response.Cookies.Append("A", "A");
context.Response.Cookies.Append("B", "B", new CookieOptions { SameSite = Http.SameSiteMode.None });
context.Response.Cookies.Append("C", "C", new CookieOptions());
context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
return Task.FromResult(0);
};
[Fact]
public async Task SecureAlwaysSetsSecure()
@ -50,10 +59,10 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
Assert.Equal("A=A; path=/; secure; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; secure; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; secure; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure; samesite=strict", transaction.SetCookie[3]);
}));
}
@ -70,10 +79,10 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure; samesite=strict", transaction.SetCookie[3]);
}));
}
@ -90,19 +99,19 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/", transaction.SetCookie[3]);
Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]);
}),
new RequestTest("https://example.com/secureSame",
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; secure", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; secure", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; secure", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure", transaction.SetCookie[3]);
Assert.Equal("A=A; path=/; secure; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; secure; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; secure; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; secure; samesite=strict", transaction.SetCookie[3]);
}));
}
@ -119,10 +128,10 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; httponly", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; httponly", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; httponly", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]);
Assert.Equal("A=A; path=/; samesite=strict; httponly", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=strict; httponly", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=strict; httponly", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=strict; httponly", transaction.SetCookie[3]);
}));
}
@ -137,12 +146,75 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
HttpCookieAppends,
new RequestTest("http://example.com/httpOnlyNone",
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=strict; httponly", transaction.SetCookie[3]);
}));
}
[Fact]
public async Task SameSiteStrictSetsItAlways()
{
await RunTest("/sameSiteStrict",
new CookiePolicyOptions
{
MinimumSameSitePolicy = Http.SameSiteMode.Strict
},
SameSiteCookieAppends,
new RequestTest("http://example.com/sameSiteStrict",
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=strict", transaction.SetCookie[3]);
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
}));
}
[Fact]
public async Task SameSiteLaxSetsItAlways()
{
await RunTest("/sameSiteLax",
new CookiePolicyOptions
{
MinimumSameSitePolicy = Http.SameSiteMode.Lax
},
SameSiteCookieAppends,
new RequestTest("http://example.com/sameSiteLax",
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/; samesite=lax", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
}));
}
[Fact]
public async Task SameSiteNoneLeavesItAlone()
{
await RunTest("/sameSiteNone",
new CookiePolicyOptions
{
MinimumSameSitePolicy = Http.SameSiteMode.None
},
SameSiteCookieAppends,
new RequestTest("http://example.com/sameSiteNone",
transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; httponly", transaction.SetCookie[3]);
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
}));
}
@ -170,10 +242,10 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
var transaction = await server.SendAsync("http://example.com/login");
Assert.NotNull(transaction.SetCookie);
Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[0]);
Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[1]);
Assert.Equal("Hao=Hao; path=/", transaction.SetCookie[2]);
Assert.Equal("Hao=Hao; path=/; secure", transaction.SetCookie[3]);
Assert.Equal("Hao=Hao; path=/; samesite=strict", transaction.SetCookie[0]);
Assert.Equal("Hao=Hao; path=/; samesite=strict", transaction.SetCookie[1]);
Assert.Equal("Hao=Hao; path=/; samesite=strict", transaction.SetCookie[2]);
Assert.Equal("Hao=Hao; path=/; secure; samesite=strict", transaction.SetCookie[3]);
}
[Fact]
@ -201,7 +273,7 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
Assert.NotNull(transaction.SetCookie);
Assert.Equal(1, transaction.SetCookie.Count);
Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/", transaction.SetCookie[0]);
Assert.Equal("A=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; samesite=lax", transaction.SetCookie[0]);
}
[Fact]