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>
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>
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
/// </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,
Expires = options.Expires,
Secure = options.Secure,
HttpOnly = options.HttpOnly,
SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
HttpOnly = options.HttpOnly
};
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 PathToken = "path";
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 SeparatorToken = "; ";
private const string EqualsToken = "=";
@ -87,15 +91,18 @@ namespace Microsoft.Net.Http.Headers
public bool Secure { get; set; }
public SameSiteMode SameSite { 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()
{
var length = _name.Length + EqualsToken.Length + _value.Length;
string expires = null;
string maxAge = null;
string sameSite = null;
if (Expires.HasValue)
{
@ -124,6 +131,12 @@ namespace Microsoft.Net.Http.Headers
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)
{
length += SeparatorToken.Length + HttpOnlyToken.Length;
@ -160,6 +173,11 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(ref sb, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(ref sb, SameSiteToken, sameSite);
}
if (HttpOnly)
{
AppendSegment(ref sb, HttpOnlyToken, null);
@ -218,6 +236,11 @@ namespace Microsoft.Net.Http.Headers
AppendSegment(builder, SecureToken, null);
}
if (SameSite != SameSiteMode.None)
{
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
}
if (HttpOnly)
{
AppendSegment(builder, HttpOnlyToken, null);
@ -267,7 +290,7 @@ namespace Microsoft.Net.Http.Headers
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)
{
Contract.Requires(startIndex >= 0);
@ -322,7 +345,7 @@ namespace Microsoft.Net.Http.Headers
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);
if (itemLength == 0)
{
@ -402,6 +425,28 @@ namespace Microsoft.Net.Http.Headers
{
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"
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(Path, other.Path, StringComparison.OrdinalIgnoreCase)
&& Secure == other.Secure
&& SameSite == other.SameSite
&& HttpOnly == other.HttpOnly;
}
@ -471,6 +517,7 @@ namespace Microsoft.Net.Http.Headers
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode()
^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();
}
}

View File

@ -20,12 +20,13 @@ namespace Microsoft.Net.Http.Headers
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
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", "");
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");
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;
}
}
@ -106,12 +120,13 @@ namespace Microsoft.Net.Http.Headers
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
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 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 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, header1 }.ToList(), new[] { string1, 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[] { 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[] { 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;
}
@ -152,12 +187,13 @@ namespace Microsoft.Net.Http.Headers
{
Domain = "domain1",
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
SameSite = SameSiteMode.Strict,
HttpOnly = true,
MaxAge = TimeSpan.FromDays(1),
Path = "path1",
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 string2 = "name2=value2";