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

Commit 82f4a68

Browse files
committed
Add SameSite attribute to SetCookie header
1 parent 9313a02 commit 82f4a68

File tree

6 files changed

+82
-7
lines changed

6 files changed

+82
-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.
48+
/// </summary>
49+
/// <returns>The <see cref="SameSiteEnforcementMode"/> representing the enforcement mode of the cookie.</returns>
50+
public SameSiteEnforcementMode SameSite { get; set; }
51+
4552
/// <summary>
4653
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
4754
/// </summary>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
public enum SameSiteEnforcementMode
7+
{
8+
None = 0,
9+
Lax,
10+
Strict
11+
}
12+
}

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 = options.SameSite,
66+
HttpOnly = options.HttpOnly
6667
};
6768

6869
var cookieValue = setCookieHeaderValue.ToString();

src/Microsoft.Net.Http.Headers/Microsoft.Net.Http.Headers.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
<EnableApiCheck>false</EnableApiCheck>
1313
</PropertyGroup>
1414

15+
<ItemGroup>
16+
<ProjectReference Include="..\Microsoft.AspNetCore.Http.Features\Microsoft.AspNetCore.Http.Features.csproj" />
17+
</ItemGroup>
18+
1519
<ItemGroup>
1620
<PackageReference Include="Microsoft.Extensions.Primitives" Version="$(AspNetCoreVersion)" />
1721
</ItemGroup>

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

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics.Contracts;
77
using System.Text;
8+
using Microsoft.AspNetCore.Http;
89
using Microsoft.Extensions.Primitives;
910

1011
namespace Microsoft.Net.Http.Headers
@@ -17,6 +18,9 @@ public class SetCookieHeaderValue
1718
private const string DomainToken = "domain";
1819
private const string PathToken = "path";
1920
private const string SecureToken = "secure";
21+
private const string SameSiteToken = "samesite";
22+
private static readonly string SameSiteLaxToken = SameSiteEnforcementMode.Lax.ToString();
23+
private static readonly string SameSiteStrictToken = SameSiteEnforcementMode.Strict.ToString();
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 SameSiteEnforcementMode 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 != SameSiteEnforcementMode.None)
135+
{
136+
sameSite = SameSite == SameSiteEnforcementMode.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 != SameSiteEnforcementMode.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 != SameSiteEnforcementMode.None)
240+
{
241+
AppendSegment(builder, SameSiteToken, SameSite == SameSiteEnforcementMode.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 = SameSiteEnforcementMode.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 = SameSiteEnforcementMode.Lax;
443+
}
444+
else if (string.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
445+
{
446+
result.SameSite = SameSiteEnforcementMode.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: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Text;
8+
using Microsoft.AspNetCore.Http;
89
using Xunit;
910

1011
namespace Microsoft.Net.Http.Headers
@@ -20,12 +21,13 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
2021
{
2122
Domain = "domain1",
2223
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
24+
SameSite = SameSiteEnforcementMode.Strict,
2325
HttpOnly = true,
2426
MaxAge = TimeSpan.FromDays(1),
2527
Path = "path1",
2628
Secure = true
2729
};
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");
30+
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");
2931

3032
var header2 = new SetCookieHeaderValue("name2", "");
3133
dataset.Add(header2, "name2=");
@@ -106,12 +108,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
106108
{
107109
Domain = "domain1",
108110
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
111+
SameSite = SameSiteEnforcementMode.Lax,
109112
HttpOnly = true,
110113
MaxAge = TimeSpan.FromDays(1),
111114
Path = "path1",
112115
Secure = true
113116
};
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";
117+
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=Lax; httponly";
115118

116119
var header2 = new SetCookieHeaderValue("name2", "value2");
117120
var string2 = "name2=value2";
@@ -152,12 +155,13 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListWithInvalidS
152155
{
153156
Domain = "domain1",
154157
Expires = new DateTimeOffset(1994, 11, 6, 8, 49, 37, TimeSpan.Zero),
158+
SameSite = SameSiteEnforcementMode.Strict,
155159
HttpOnly = true,
156160
MaxAge = TimeSpan.FromDays(1),
157161
Path = "path1",
158162
Secure = true
159163
};
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";
164+
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";
161165

162166
var header2 = new SetCookieHeaderValue("name2", "value2");
163167
var string2 = "name2=value2";

0 commit comments

Comments
 (0)