diff --git a/src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs b/src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs
index 7bff4176..d9e0047d 100644
--- a/src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs
+++ b/src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs
@@ -42,6 +42,13 @@ public CookieOptions()
/// true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.
public bool Secure { get; set; }
+
+ ///
+ /// Gets or sets the value for the SameSite attribute of the cookie. The default value is
+ ///
+ /// The representing the enforcement mode of the cookie.
+ public SameSiteMode SameSite { get; set; } = SameSiteMode.Lax;
+
///
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
///
diff --git a/src/Microsoft.AspNetCore.Http.Features/SameSiteMode.cs b/src/Microsoft.AspNetCore.Http.Features/SameSiteMode.cs
new file mode 100644
index 00000000..0ae4481e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Http.Features/SameSiteMode.cs
@@ -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
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs b/src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs
index e7f2d120..04dc5b94 100644
--- a/src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs
+++ b/src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs
@@ -62,7 +62,8 @@ public void Append(string key, string value, CookieOptions options)
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();
diff --git a/src/Microsoft.Net.Http.Headers/SameSiteMode.cs b/src/Microsoft.Net.Http.Headers/SameSiteMode.cs
new file mode 100644
index 00000000..1976386c
--- /dev/null
+++ b/src/Microsoft.Net.Http.Headers/SameSiteMode.cs
@@ -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
+ }
+}
diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs
index 8c6d9a56..10c68bb5 100644
--- a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs
+++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs
@@ -17,6 +17,10 @@ public class SetCookieHeaderValue
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 @@ public string Value
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 @@ public override string ToString()
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 @@ public override string ToString()
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 @@ public void AppendToStringBuilder(StringBuilder builder)
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 @@ public static bool TryParseStrictList(IList inputs, out IList= 0);
@@ -322,7 +345,7 @@ private static int GetSetCookieLength(string input, int startIndex, out SetCooki
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 @@ private static int GetSetCookieLength(string input, int startIndex, out SetCooki
{
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 @@ public override bool Equals(object obj)
&& 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 @@ public override int GetHashCode()
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
^ Secure.GetHashCode()
+ ^ SameSite.GetHashCode()
^ HttpOnly.GetHashCode();
}
}
diff --git a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs
index a3dad09a..2a84397b 100644
--- a/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs
+++ b/test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs
@@ -20,12 +20,13 @@ public static TheoryData SetCookieHeaderDataSet
{
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 @@ public static TheoryData SetCookieHeaderDataSet
};
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 @@ public static TheoryData, string[]> ListOfSetCookieH
{
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 @@ public static TheoryData, string[]> ListOfSetCookieH
};
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 @@ public static TheoryData, string[]> ListOfSetCookieH
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 @@ public static TheoryData, string[]> ListWithInvalidS
{
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";