Skip to content

Commit e2daf35

Browse files
committed
Re-implement SameSite for 2019 #12125
1 parent f7a7c98 commit e2daf35

File tree

7 files changed

+188
-25
lines changed

7 files changed

+188
-25
lines changed

src/Http/Headers/src/SetCookieHeaderValue.cs

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,14 @@ public class SetCookieHeaderValue
1919
private const string SecureToken = "secure";
2020
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
2121
private const string SameSiteToken = "samesite";
22+
private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLower();
2223
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
2324
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
25+
26+
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
27+
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
28+
internal static bool UseSameSite2016Compat;
29+
2430
private const string HttpOnlyToken = "httponly";
2531
private const string SeparatorToken = "; ";
2632
private const string EqualsToken = "=";
@@ -34,6 +40,14 @@ private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValuePars
3440
private StringSegment _name;
3541
private StringSegment _value;
3642

43+
static SetCookieHeaderValue()
44+
{
45+
if (AppContext.TryGetSwitch("Microsoft.Net.Http.Headers.SetCookieHeaderValue.UseSameSite2016Compat", out var enabled))
46+
{
47+
UseSameSite2016Compat = enabled;
48+
}
49+
}
50+
3751
private SetCookieHeaderValue()
3852
{
3953
// Used by the parser to create a new instance of this type.
@@ -90,11 +104,11 @@ public StringSegment Value
90104

91105
public bool Secure { get; set; }
92106

93-
public SameSiteMode SameSite { get; set; }
107+
public SameSiteMode SameSite { get; set; } = UseSameSite2016Compat ? SameSiteMode.None : (SameSiteMode)(-1); // Unspecified
94108

95109
public bool HttpOnly { get; set; }
96110

97-
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
111+
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
98112
public override string ToString()
99113
{
100114
var length = _name.Length + EqualsToken.Length + _value.Length;
@@ -130,9 +144,20 @@ public override string ToString()
130144
length += SeparatorToken.Length + SecureToken.Length;
131145
}
132146

133-
if (SameSite != SameSiteMode.None)
147+
// Allow for Unspecified (-1) to skip SameSite
148+
if (SameSite == SameSiteMode.None && !UseSameSite2016Compat)
149+
{
150+
sameSite = SameSiteNoneToken;
151+
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
152+
}
153+
else if (SameSite == SameSiteMode.Lax)
134154
{
135-
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
155+
sameSite = SameSiteLaxToken;
156+
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
157+
}
158+
else if (SameSite == SameSiteMode.Strict)
159+
{
160+
sameSite = SameSiteStrictToken;
136161
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
137162
}
138163

@@ -172,7 +197,7 @@ public override string ToString()
172197
AppendSegment(ref sb, SecureToken, null);
173198
}
174199

175-
if (SameSite != SameSiteMode.None)
200+
if (sameSite != null)
176201
{
177202
AppendSegment(ref sb, SameSiteToken, sameSite);
178203
}
@@ -235,9 +260,18 @@ public void AppendToStringBuilder(StringBuilder builder)
235260
AppendSegment(builder, SecureToken, null);
236261
}
237262

238-
if (SameSite != SameSiteMode.None)
263+
// Allow for Unspecified (-1) to skip SameSite
264+
if (SameSite == SameSiteMode.None && !UseSameSite2016Compat)
265+
{
266+
AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
267+
}
268+
else if (SameSite == SameSiteMode.Lax)
239269
{
240-
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
270+
AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
271+
}
272+
else if (SameSite == SameSiteMode.Strict)
273+
{
274+
AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
241275
}
242276

243277
if (HttpOnly)
@@ -289,7 +323,7 @@ public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieH
289323
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
290324
}
291325

292-
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
326+
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly
293327
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
294328
{
295329
Contract.Requires(startIndex >= 0);
@@ -424,25 +458,34 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
424458
{
425459
result.Secure = true;
426460
}
427-
// samesite-av = "SameSite" / "SameSite=" samesite-value
428-
// samesite-value = "Strict" / "Lax"
461+
// samesite-av = "SameSite=" samesite-value
462+
// samesite-value = "Strict" / "Lax" / "None"
429463
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
430464
{
431465
if (!ReadEqualsSign(input, ref offset))
432466
{
433-
result.SameSite = SameSiteMode.Strict;
467+
result.SameSite = UseSameSite2016Compat ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
434468
}
435469
else
436470
{
437471
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
438472

439-
if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
473+
if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
474+
{
475+
result.SameSite = SameSiteMode.Strict;
476+
}
477+
else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
440478
{
441479
result.SameSite = SameSiteMode.Lax;
442480
}
481+
else if (!UseSameSite2016Compat
482+
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
483+
{
484+
result.SameSite = SameSiteMode.None;
485+
}
443486
else
444487
{
445-
result.SameSite = SameSiteMode.Strict;
488+
result.SameSite = UseSameSite2016Compat ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
446489
}
447490
}
448491
}
@@ -520,4 +563,4 @@ public override int GetHashCode()
520563
^ HttpOnly.GetHashCode();
521564
}
522565
}
523-
}
566+
}

src/Http/Headers/test/SetCookieHeaderValueTest.cs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
5757
{
5858
SameSite = SameSiteMode.None,
5959
};
60-
dataset.Add(header7, "name7=value7");
60+
dataset.Add(header7, "name7=value7; samesite=none");
6161

6262

6363
return dataset;
@@ -155,9 +155,20 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
155155
{
156156
SameSite = SameSiteMode.Strict
157157
};
158-
var string6a = "name6=value6; samesite";
159-
var string6b = "name6=value6; samesite=Strict";
160-
var string6c = "name6=value6; samesite=invalid";
158+
var string6 = "name6=value6; samesite=Strict";
159+
160+
var header7 = new SetCookieHeaderValue("name7", "value7")
161+
{
162+
SameSite = SameSiteMode.None
163+
};
164+
var string7 = "name7=value7; samesite=None";
165+
166+
var header8 = new SetCookieHeaderValue("name8", "value8")
167+
{
168+
SameSite = (SameSiteMode)(-1) // Unspecified
169+
};
170+
var string8a = "name8=value8; samesite";
171+
var string8b = "name8=value8; samesite=invalid";
161172

162173
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
163174
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
@@ -170,9 +181,10 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
170181
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
171182
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
172183
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 });
184+
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
185+
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
186+
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
187+
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
176188

177189
return dataset;
178190
}
@@ -301,6 +313,28 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp
301313
Assert.Equal(expectedValue, input.ToString());
302314
}
303315

316+
[Fact]
317+
public void SetCookieHeaderValue_ToString_SameSite2016Compat()
318+
{
319+
SetCookieHeaderValue.UseSameSite2016Compat = true;
320+
321+
var input = new SetCookieHeaderValue("name", "value")
322+
{
323+
SameSite = SameSiteMode.None,
324+
};
325+
326+
Assert.Equal("name=value", input.ToString());
327+
328+
SetCookieHeaderValue.UseSameSite2016Compat = false;
329+
330+
var input2 = new SetCookieHeaderValue("name", "value")
331+
{
332+
SameSite = SameSiteMode.None,
333+
};
334+
335+
Assert.Equal("name=value; samesite=none", input2.ToString());
336+
}
337+
304338
[Theory]
305339
[MemberData(nameof(SetCookieHeaderDataSet))]
306340
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
@@ -312,6 +346,32 @@ public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue inpu
312346
Assert.Equal(expectedValue, builder.ToString());
313347
}
314348

349+
[Fact]
350+
public void SetCookieHeaderValue_AppendToStringBuilder_SameSite2016Compat()
351+
{
352+
SetCookieHeaderValue.UseSameSite2016Compat = true;
353+
354+
var builder = new StringBuilder();
355+
var input = new SetCookieHeaderValue("name", "value")
356+
{
357+
SameSite = SameSiteMode.None,
358+
};
359+
360+
input.AppendToStringBuilder(builder);
361+
Assert.Equal("name=value", builder.ToString());
362+
363+
SetCookieHeaderValue.UseSameSite2016Compat = false;
364+
365+
var builder2 = new StringBuilder();
366+
var input2 = new SetCookieHeaderValue("name", "value")
367+
{
368+
SameSite = SameSiteMode.None,
369+
};
370+
371+
input2.AppendToStringBuilder(builder2);
372+
Assert.Equal("name=value; samesite=none", builder2.ToString());
373+
}
374+
315375
[Theory]
316376
[MemberData(nameof(SetCookieHeaderDataSet))]
317377
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -322,6 +382,31 @@ public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue c
322382
Assert.Equal(expectedValue, header.ToString());
323383
}
324384

385+
[Fact]
386+
public void SetCookieHeaderValue_Parse_AcceptsValidValues_SameSite2016Compat()
387+
{
388+
SetCookieHeaderValue.UseSameSite2016Compat = true;
389+
var header = SetCookieHeaderValue.Parse("name=value; samesite=none");
390+
391+
var cookie = new SetCookieHeaderValue("name", "value")
392+
{
393+
SameSite = SameSiteMode.Strict,
394+
};
395+
396+
Assert.Equal(cookie, header);
397+
Assert.Equal("name=value; samesite=strict", header.ToString());
398+
SetCookieHeaderValue.UseSameSite2016Compat = false;
399+
400+
var header2 = SetCookieHeaderValue.Parse("name=value; samesite=none");
401+
402+
var cookie2 = new SetCookieHeaderValue("name", "value")
403+
{
404+
SameSite = SameSiteMode.None,
405+
};
406+
Assert.Equal(cookie2, header2);
407+
Assert.Equal("name=value; samesite=none", header2.ToString());
408+
}
409+
325410
[Theory]
326411
[MemberData(nameof(SetCookieHeaderDataSet))]
327412
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -332,6 +417,31 @@ public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValu
332417
Assert.Equal(expectedValue, header.ToString());
333418
}
334419

420+
[Fact]
421+
public void SetCookieHeaderValue_TryParse_AcceptsValidValues_SameSite2016Compat()
422+
{
423+
SetCookieHeaderValue.UseSameSite2016Compat = true;
424+
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header));
425+
var cookie = new SetCookieHeaderValue("name", "value")
426+
{
427+
SameSite = SameSiteMode.Strict,
428+
};
429+
430+
Assert.Equal(cookie, header);
431+
Assert.Equal("name=value; samesite=strict", header.ToString());
432+
433+
SetCookieHeaderValue.UseSameSite2016Compat = false;
434+
435+
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header2));
436+
var cookie2 = new SetCookieHeaderValue("name", "value")
437+
{
438+
SameSite = SameSiteMode.None,
439+
};
440+
441+
Assert.Equal(cookie2, header2);
442+
Assert.Equal("name=value; samesite=none", header2.ToString());
443+
}
444+
335445
[Theory]
336446
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
337447
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)

src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/OpenIdConnectSample.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk.Web">
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
22

33
<PropertyGroup>
44
<TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
@@ -21,6 +21,7 @@
2121
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
2222
<Reference Include="Microsoft.Extensions.Logging.Console" />
2323
<Reference Include="Microsoft.Extensions.Logging.Debug" />
24+
<Reference Include="Microsoft.Net.Http.Headers" />
2425
</ItemGroup>
2526

2627
<ItemGroup>

src/Security/Authentication/test/CookieTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
649649
Assert.Contains(" path=/foo", setCookie1);
650650
Assert.Contains(" domain=another.com", setCookie1);
651651
Assert.Contains(" secure", setCookie1);
652-
Assert.DoesNotContain(" samesite", setCookie1);
652+
Assert.Contains(" samesite=none", setCookie1);
653653
Assert.Contains(" httponly", setCookie1);
654654

655655
var server2 = CreateServer(o =>

src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
@@ -39,6 +39,7 @@
3939
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
4040
<Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" />
4141
<Reference Include="Microsoft.AspNetCore.TestHost" />
42+
<Reference Include="Microsoft.Net.Http.Headers" />
4243
</ItemGroup>
4344

4445
</Project>

src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavio
376376
var server = settings.CreateTestServer();
377377
var transaction = await server.SendAsync(ChallengeEndpoint);
378378

379+
Assert.Contains("samesite=none", transaction.SetCookie.First());
379380
var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
380381
var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
381382
Assert.True(nonceCookie.Expires.HasValue);
@@ -613,4 +614,4 @@ public async Task Challenge_HasOverwrittenMaxAgeParaFromBaseAuthenticationProper
613614
Assert.Contains("max_age=1234", res.Headers.Location.Query);
614615
}
615616
}
616-
}
617+
}

0 commit comments

Comments
 (0)