Skip to content
This repository was archived by the owner on Nov 20, 2018. It is now read-only.

Adding SameSite attribute to SetCookies header #843

Merged
merged 1 commit into from
May 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public CookieOptions()
/// <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>
Expand Down
14 changes: 14 additions & 0 deletions src/Microsoft.AspNetCore.Http.Features/SameSiteMode.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
3 changes: 2 additions & 1 deletion src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 13 additions & 0 deletions src/Microsoft.Net.Http.Headers/SameSiteMode.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
53 changes: 50 additions & 3 deletions src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "=";
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -267,7 +290,7 @@ public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieH
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);
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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;
}

Expand All @@ -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();
}
}
Expand Down
42 changes: 39 additions & 3 deletions test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ public static TheoryData<SetCookieHeaderValue, string> 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=");
Expand All @@ -46,6 +47,19 @@ public static TheoryData<SetCookieHeaderValue, string> 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;
}
}
Expand Down Expand Up @@ -106,12 +120,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, 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";
Expand All @@ -129,6 +144,21 @@ public static TheoryData<IList<SetCookieHeaderValue>, 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 });
Expand All @@ -138,6 +168,11 @@ public static TheoryData<IList<SetCookieHeaderValue>, 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;
}
Expand All @@ -152,12 +187,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, 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";
Expand Down