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

Commit e8123db

Browse files
committed
Add SameSite attribute to SetCookie header
1 parent 6e87b0f commit e8123db

File tree

6 files changed

+125
-7
lines changed

6 files changed

+125
-7
lines changed

src/Microsoft.AspNetCore.Http.Features/CookieOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ public CookieOptions()
4242
/// <returns>true to transmit the cookie only over an SSL connection (HTTPS); otherwise, false.</returns>
4343
public bool Secure { get; set; }
4444

45+
46+
/// <summary>
47+
/// Gets or sets the value for the SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Lax"/>
48+
/// </summary>
49+
/// <returns>The <see cref="SameSiteMode"/> representing the enforcement mode of the cookie.</returns>
50+
public SameSiteMode SameSite { get; set; } = SameSiteMode.Lax;
51+
4552
/// <summary>
4653
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
4754
/// </summary>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Http
5+
{
6+
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
7+
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
8+
public enum SameSiteMode
9+
{
10+
None = 0,
11+
Lax,
12+
Strict
13+
}
14+
}

src/Microsoft.AspNetCore.Http/Internal/ResponseCookies.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public void Append(string key, string value, CookieOptions options)
6262
Path = options.Path,
6363
Expires = options.Expires,
6464
Secure = options.Secure,
65-
HttpOnly = options.HttpOnly,
65+
SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
66+
HttpOnly = options.HttpOnly
6667
};
6768

6869
var cookieValue = setCookieHeaderValue.ToString();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.Net.Http.Headers
5+
{
6+
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
7+
public enum SameSiteMode
8+
{
9+
None = 0,
10+
Lax,
11+
Strict
12+
}
13+
}

src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public class SetCookieHeaderValue
1717
private const string DomainToken = "domain";
1818
private const string PathToken = "path";
1919
private const string SecureToken = "secure";
20+
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
21+
private const string SameSiteToken = "samesite";
22+
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
23+
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
2024
private const string HttpOnlyToken = "httponly";
2125
private const string SeparatorToken = "; ";
2226
private const string EqualsToken = "=";
@@ -87,15 +91,18 @@ public string Value
8791

8892
public bool Secure { get; set; }
8993

94+
public SameSiteMode SameSite { get; set; }
95+
9096
public bool HttpOnly { get; set; }
9197

92-
// name="val ue"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly
98+
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
9399
public override string ToString()
94100
{
95101
var length = _name.Length + EqualsToken.Length + _value.Length;
96102

97103
string expires = null;
98104
string maxAge = null;
105+
string sameSite = null;
99106

100107
if (Expires.HasValue)
101108
{
@@ -124,6 +131,12 @@ public override string ToString()
124131
length += SeparatorToken.Length + SecureToken.Length;
125132
}
126133

134+
if (SameSite != SameSiteMode.None)
135+
{
136+
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
137+
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
138+
}
139+
127140
if (HttpOnly)
128141
{
129142
length += SeparatorToken.Length + HttpOnlyToken.Length;
@@ -160,6 +173,11 @@ public override string ToString()
160173
AppendSegment(ref sb, SecureToken, null);
161174
}
162175

176+
if (SameSite != SameSiteMode.None)
177+
{
178+
AppendSegment(ref sb, SameSiteToken, sameSite);
179+
}
180+
163181
if (HttpOnly)
164182
{
165183
AppendSegment(ref sb, HttpOnlyToken, null);
@@ -218,6 +236,11 @@ public void AppendToStringBuilder(StringBuilder builder)
218236
AppendSegment(builder, SecureToken, null);
219237
}
220238

239+
if (SameSite != SameSiteMode.None)
240+
{
241+
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
242+
}
243+
221244
if (HttpOnly)
222245
{
223246
AppendSegment(builder, HttpOnlyToken, null);
@@ -267,7 +290,7 @@ public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieH
267290
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
268291
}
269292

270-
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; httponly
293+
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
271294
private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue)
272295
{
273296
Contract.Requires(startIndex >= 0);
@@ -322,7 +345,7 @@ private static int GetSetCookieLength(string input, int startIndex, out SetCooki
322345

323346
offset += HttpRuleParser.GetWhitespaceLength(input, offset);
324347

325-
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / httponly-av / extension-av
348+
// cookie-av = expires-av / max-age-av / domain-av / path-av / secure-av / samesite-av / httponly-av / extension-av
326349
itemLength = HttpRuleParser.GetTokenLength(input, offset);
327350
if (itemLength == 0)
328351
{
@@ -402,6 +425,28 @@ private static int GetSetCookieLength(string input, int startIndex, out SetCooki
402425
{
403426
result.Secure = true;
404427
}
428+
// samesite-av = "SameSite" / "SameSite=" samesite-value
429+
// samesite-value = "Strict" / "Lax"
430+
else if (string.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
431+
{
432+
if (!ReadEqualsSign(input, ref offset))
433+
{
434+
result.SameSite = SameSiteMode.Strict;
435+
}
436+
else
437+
{
438+
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
439+
440+
if (string.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
441+
{
442+
result.SameSite = SameSiteMode.Lax;
443+
}
444+
else
445+
{
446+
result.SameSite = SameSiteMode.Strict;
447+
}
448+
}
449+
}
405450
// httponly-av = "HttpOnly"
406451
else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase))
407452
{
@@ -459,6 +504,7 @@ public override bool Equals(object obj)
459504
&& string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase)
460505
&& string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase)
461506
&& Secure == other.Secure
507+
&& SameSite == other.SameSite
462508
&& HttpOnly == other.HttpOnly;
463509
}
464510

@@ -471,6 +517,7 @@ public override int GetHashCode()
471517
^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0)
472518
^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0)
473519
^ Secure.GetHashCode()
520+
^ SameSite.GetHashCode()
474521
^ HttpOnly.GetHashCode();
475522
}
476523
}

test/Microsoft.Net.Http.Headers.Tests/SetCookieHeaderValueTest.cs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
2020
{
2121
Domain = "domain1",
2222
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
23+
SameSite = SameSiteMode.Strict,
2324
HttpOnly = true,
2425
MaxAge = TimeSpan.FromDays(1),
2526
Path = "path1",
2627
Secure = true
2728
};
28-
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");
29+
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");
2930

3031
var header2 = new SetCookieHeaderValue("name2", "");
3132
dataset.Add(header2, "name2=");
@@ -46,6 +47,19 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
4647
};
4748
dataset.Add(header5, "name5=value5; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1");
4849

50+
var header6 = new SetCookieHeaderValue("name6", "value6")
51+
{
52+
SameSite = SameSiteMode.Lax,
53+
};
54+
dataset.Add(header6, "name6=value6; samesite=lax");
55+
56+
var header7 = new SetCookieHeaderValue("name7", "value7")
57+
{
58+
SameSite = SameSiteMode.None,
59+
};
60+
dataset.Add(header7, "name7=value7");
61+
62+
4963
return dataset;
5064
}
5165
}
@@ -106,12 +120,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
106120
{
107121
Domain = "domain1",
108122
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
123+
SameSite = SameSiteMode.Strict,
109124
HttpOnly = true,
110125
MaxAge = TimeSpan.FromDays(1),
111126
Path = "path1",
112127
Secure = true
113128
};
114-
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";
129+
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";
115130

116131
var header2 = new SetCookieHeaderValue("name2", "value2");
117132
var string2 = "name2=value2";
@@ -129,6 +144,21 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
129144
};
130145
var string4 = "name4=value4; expires=Sun, 06 Nov 1994 08:49:37 GMT; domain=domain1";
131146

147+
var header5 = new SetCookieHeaderValue("name5", "value5")
148+
{
149+
SameSite = SameSiteMode.Lax
150+
};
151+
var string5a = "name5=value5; samesite=lax";
152+
var string5b = "name5=value5; samesite=Lax";
153+
154+
var header6 = new SetCookieHeaderValue("name6", "value6")
155+
{
156+
SameSite = SameSiteMode.Strict
157+
};
158+
var string6a = "name6=value6; samesite";
159+
var string6b = "name6=value6; samesite=Strict";
160+
var string6c = "name6=value6; samesite=invalid";
161+
132162
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
133163
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
134164
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ",", " , ", string1 });
@@ -138,6 +168,11 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
138168
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + ", " + string1 });
139169
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
140170
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
171+
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
172+
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
173+
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
174+
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
175+
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
141176

142177
return dataset;
143178
}
@@ -152,12 +187,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListWithInvalidS
152187
{
153188
Domain = "domain1",
154189
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
190+
SameSite = SameSiteMode.Strict,
155191
HttpOnly = true,
156192
MaxAge = TimeSpan.FromDays(1),
157193
Path = "path1",
158194
Secure = true
159195
};
160-
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";
196+
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";
161197

162198
var header2 = new SetCookieHeaderValue("name2", "value2");
163199
var string2 = "name2=value2";

0 commit comments

Comments
 (0)