Add SameSite attribute to SetCookie header

This commit is contained in:
John Luo 2017-05-17 20:03:34 -07:00
parent 6e87b0f5eb
commit e8123db21e
6 changed files with 125 additions and 7 deletions

View File

@ -42,6 +42,13 @@ namespace Microsoft.AspNetCore.Http
/// <returns>true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.</returns> /// <returns>true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.</returns>
public bool Secure { get; set; } public bool Secure { get; set; }
/// <summary>
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Lax"/>
/// </summary>
/// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns>
public SameSiteMode SameSite { get; set; } = SameSiteMode.Lax;
/// <summary> /// <summary>
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script. /// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
/// </summary> /// </summary>

View File

@ -0,0 +1,14 @@
// 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.
namespace Microsoft.AspNetCore.Http
{
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
public enum SameSiteMode
{
None = 0,
Lax,
Strict
}
}

View File

@ -62,7 +62,8 @@ namespace Microsoft.AspNetCore.Http.Internal
Path = options.Path, Path = options.Path,
Expires = options.Expires, Expires = options.Expires,
Secure = options.Secure, Secure = options.Secure,
HttpOnly = options.HttpOnly, SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
HttpOnly = options.HttpOnly
}; };
var cookieValue = setCookieHeaderValue.ToString(); var cookieValue = setCookieHeaderValue.ToString();

View File

@ -0,0 +1,13 @@
// 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.
namespace Microsoft.Net.Http.Headers
{
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
public enum SameSiteMode
{
None = 0,
Lax,
Strict
}
}

View File

@ -17,6 +17,10 @@ namespace Microsoft.Net.Http.Headers
private const string DomainToken = "domain"; private const string DomainToken = "domain";
private const string PathToken = "path"; private const string PathToken = "path";
private const string SecureToken = "secure"; private const string SecureToken = "secure";
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
private const string SameSiteToken = "samesite";
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
private const string HttpOnlyToken = "httponly"; private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; "; private const string SeparatorToken = "; ";
private const string EqualsToken = "="; private const string EqualsToken = "=";
@ -87,15 +91,18 @@ namespace Microsoft.Net.Http.Headers
public bool Secure { get; set; } public bool Secure { get; set; }
public SameSiteMode SameSite { get; set; }
public bool HttpOnly { get; set; } public bool HttpOnly { get; set; }
// name="val ue"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
public override string ToString() public override string ToString()
{ {
var length = _name.Length + EqualsToken.Length + _value.Length; var length = _name.Length + EqualsToken.Length + _value.Length;
string expires = null; string expires = null;
string maxAge = null; string maxAge = null;
string sameSite = null;
if (Expires.HasValue) if (Expires.HasValue)
{ {
@ -124,6 +131,12 @@ namespace Microsoft.Net.Http.Headers
length += SeparatorToken.Length + SecureToken.Length; length += SeparatorToken.Length + SecureToken.Length;
} }
if (SameSite != SameSiteMode.None)
{
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
if (HttpOnly) if (HttpOnly)
{ {
length += SeparatorToken.Length + HttpOnlyToken.Length; length += SeparatorToken.Length + HttpOnlyToken.Length;
@ -160,6 +173,11 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(ref sb, SecureToken, null); AppendSegment(ref sb, SecureToken, null);
} }
if (SameSite != SameSiteMode.None)
{
AppendSegment(ref sb, SameSiteToken, sameSite);
}
if (HttpOnly) if (HttpOnly)
{ {
AppendSegment(ref sb, HttpOnlyToken, null); AppendSegment(ref sb, HttpOnlyToken, null);
@ -218,6 +236,11 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(builder, SecureToken, null); AppendSegment(builder, SecureToken, null);
} }
if (SameSite != SameSiteMode.None)
{
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
}
if (HttpOnly) if (HttpOnly)
{ {
AppendSegment(builder, HttpOnlyToken, null); AppendSegment(builder, HttpOnlyToken, null);
@ -267,7 +290,7 @@ namespace Microsoft.Net.Http.Headers
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
} }
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly // name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue) private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue)
{ {
Contract.Requires(startIndex >= 0); Contract.Requires(startIndex >= 0);
@ -322,7 +345,7 @@ namespace Microsoft.Net.Http.Headers
offset += HttpRuleParser.GetWhitespaceLength(input, offset); offset += HttpRuleParser.GetWhitespaceLength(input, offset);
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / httponly-av / extension-av // cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av
itemLength = HttpRuleParser.GetTokenLength(input, offset); itemLength = HttpRuleParser.GetTokenLength(input, offset);
if (itemLength == 0) if (itemLength == 0)
{ {
@ -402,6 +425,28 @@ namespace Microsoft.Net.Http.Headers
{ {
result.Secure = true; result.Secure = true;
} }
// samesite-av = "SameSite" / "SameSite=" samesite-value
// samesite-value = "Strict" / "Lax"
else if (string.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
{
if (!ReadEqualsSign(input, ref offset))
{
result.SameSite = SameSiteMode.Strict;
}
else
{
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
if (string.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Lax;
}
else
{
result.SameSite = SameSiteMode.Strict;
}
}
}
// httponly-av = "HttpOnly" // httponly-av = "HttpOnly"
else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase))
{ {
@ -459,6 +504,7 @@ namespace Microsoft.Net.Http.Headers
&& string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) && string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)
&& string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) && string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
&& Secure == other.Secure && Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly; && HttpOnly == other.HttpOnly;
} }
@ -471,6 +517,7 @@ namespace Microsoft.Net.Http.Headers
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) ^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) ^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode() ^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode(); ^ HttpOnly.GetHashCode();
} }
} }

View File

@ -20,12 +20,13 @@ namespace Microsoft.Net.Http.Headers
{ {
Domain = "domain1", Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true, HttpOnly = true,
MaxAge = TimeSpan.FromDays(1), MaxAge = TimeSpan.FromDays(1),
Path = "path1", Path = "path1",
Secure = true Secure = true
}; };
dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly"); dataset.Add(header1, "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly");
var header2 = new SetCookieHeaderValue("name2", ""); var header2 = new SetCookieHeaderValue("name2", "");
dataset.Add(header2, "name2="); dataset.Add(header2, "name2=");
@ -46,6 +47,19 @@ namespace Microsoft.Net.Http.Headers
}; };
dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"); dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1");
var header6 = new SetCookieHeaderValue("name6", "value6")
{
SameSite = SameSiteMode.Lax,
};
dataset.Add(header6, "name6=value6; samesite=lax");
var header7 = new SetCookieHeaderValue("name7", "value7")
{
SameSite = SameSiteMode.None,
};
dataset.Add(header7, "name7=value7");
return dataset; return dataset;
} }
} }
@ -106,12 +120,13 @@ namespace Microsoft.Net.Http.Headers
{ {
Domain = "domain1", Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true, HttpOnly = true,
MaxAge = TimeSpan.FromDays(1), MaxAge = TimeSpan.FromDays(1),
Path = "path1", Path = "path1",
Secure = true Secure = true
}; };
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly"; var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=strict; httponly";
var header2 = new SetCookieHeaderValue("name2", "value2"); var header2 = new SetCookieHeaderValue("name2", "value2");
var string2 = "name2=value2"; var string2 = "name2=value2";
@ -129,6 +144,21 @@ namespace Microsoft.Net.Http.Headers
}; };
var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1"; var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1";
var header5 = new SetCookieHeaderValue("name5", "value5")
{
SameSite = SameSiteMode.Lax
};
var string5a = "name5=value5; samesite=lax";
var string5b = "name5=value5; samesite=Lax";
var header6 = new SetCookieHeaderValue("name6", "value6")
{
SameSite = SameSiteMode.Strict
};
var string6a = "name6=value6; samesite";
var string6b = "name6=value6; samesite=Strict";
var string6c = "name6=value6; samesite=invalid";
dataset.Add(new[] { header1 }.ToList(), new[] { string1 }); dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 }); dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 }); dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
@ -138,6 +168,11 @@ namespace Microsoft.Net.Http.Headers
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 }); dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 }); dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) }); dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
return dataset; return dataset;
} }
@ -152,12 +187,13 @@ namespace Microsoft.Net.Http.Headers
{ {
Domain = "domain1", Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero), Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true, HttpOnly = true,
MaxAge = TimeSpan.FromDays(1), MaxAge = TimeSpan.FromDays(1),
Path = "path1", Path = "path1",
Secure = true Secure = true
}; };
var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly"; var string1 = "name1=n1=v1&n2=v2&n3=v3; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite=Strict; httponly";
var header2 = new SetCookieHeaderValue("name2", "value2"); var header2 = new SetCookieHeaderValue("name2", "value2");
var string2 = "name2=value2"; var string2 = "name2=value2";