diff --git a/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs b/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs
index 1bd100081dcf..a730eadf21e9 100644
--- a/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs
+++ b/src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs
@@ -316,6 +316,7 @@ public RangeItemHeaderValue(long? from, long? to) { }
}
public enum SameSiteMode
{
+ Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,
diff --git a/src/Http/Headers/src/SameSiteMode.cs b/src/Http/Headers/src/SameSiteMode.cs
index 29c08a5984c5..70e5fd3b2c12 100644
--- a/src/Http/Headers/src/SameSiteMode.cs
+++ b/src/Http/Headers/src/SameSiteMode.cs
@@ -5,12 +5,14 @@ namespace Microsoft.Net.Http.Headers
{
///
/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests.
- /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
+ /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
///
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
public enum SameSiteMode
{
/// No SameSite field will be set, the client should follow its default cookie policy.
+ Unspecified = -1,
+ /// Indicates the client should disable same-site restrictions.
None = 0,
/// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.
Lax,
diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs
index 852959348698..3a5b217d6c31 100644
--- a/src/Http/Headers/src/SetCookieHeaderValue.cs
+++ b/src/Http/Headers/src/SetCookieHeaderValue.cs
@@ -20,8 +20,14 @@ public class SetCookieHeaderValue
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 SameSiteNoneToken = SameSiteMode.None.ToString().ToLower();
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
+
+ // True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
+ // False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
+ internal static bool SuppressSameSiteNone;
+
private const string HttpOnlyToken = "httponly";
private const string SeparatorToken = "; ";
private const string EqualsToken = "=";
@@ -36,6 +42,14 @@ private static readonly HttpHeaderParser MultipleValuePars
private StringSegment _name;
private StringSegment _value;
+ static SetCookieHeaderValue()
+ {
+ if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
+ {
+ SuppressSameSiteNone = enabled;
+ }
+ }
+
private SetCookieHeaderValue()
{
// Used by the parser to create a new instance of this type.
@@ -92,16 +106,17 @@ public StringSegment Value
public bool Secure { get; set; }
- public SameSiteMode SameSite { get; set; }
+ public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
public bool HttpOnly { get; set; }
- // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
+ // name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
public override string ToString()
{
var length = _name.Length + EqualsToken.Length + _value.Length;
string maxAge = null;
+ string sameSite = null;
if (Expires.HasValue)
{
@@ -129,9 +144,20 @@ public override string ToString()
length += SeparatorToken.Length + SecureToken.Length;
}
- if (SameSite != SameSiteMode.None)
+ // Allow for Unspecified (-1) to skip SameSite
+ if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
+ {
+ sameSite = SameSiteNoneToken;
+ length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
+ }
+ else if (SameSite == SameSiteMode.Lax)
{
- var sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
+ sameSite = SameSiteLaxToken;
+ length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
+ }
+ else if (SameSite == SameSiteMode.Strict)
+ {
+ sameSite = SameSiteStrictToken;
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
}
@@ -140,9 +166,9 @@ public override string ToString()
length += SeparatorToken.Length + HttpOnlyToken.Length;
}
- return string.Create(length, (this, maxAge), (span, tuple) =>
+ return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
{
- var (headerValue, maxAgeValue) = tuple;
+ var (headerValue, maxAgeValue, sameSite) = tuple;
Append(ref span, headerValue._name);
Append(ref span, EqualsToken);
@@ -180,9 +206,9 @@ public override string ToString()
AppendSegment(ref span, SecureToken, null);
}
- if (headerValue.SameSite != SameSiteMode.None)
+ if (sameSite != null)
{
- AppendSegment(ref span, SameSiteToken, headerValue.SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
+ AppendSegment(ref span, SameSiteToken, sameSite);
}
if (headerValue.HttpOnly)
@@ -248,9 +274,18 @@ public void AppendToStringBuilder(StringBuilder builder)
AppendSegment(builder, SecureToken, null);
}
- if (SameSite != SameSiteMode.None)
+ // Allow for Unspecified (-1) to skip SameSite
+ if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
+ {
+ AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
+ }
+ else if (SameSite == SameSiteMode.Lax)
{
- AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
+ AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
+ }
+ else if (SameSite == SameSiteMode.Strict)
+ {
+ AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
}
if (HttpOnly)
@@ -302,7 +337,7 @@ public static bool TryParseStrictList(IList inputs, out IList= 0);
@@ -437,25 +472,34 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
{
result.Secure = true;
}
- // samesite-av = "SameSite" / "SameSite=" samesite-value
- // samesite-value = "Strict" / "Lax"
+ // samesite-av = "SameSite=" samesite-value
+ // samesite-value = "Strict" / "Lax" / "None"
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
{
if (!ReadEqualsSign(input, ref offset))
{
- result.SameSite = SameSiteMode.Strict;
+ result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
}
else
{
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
- if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
+ if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
+ {
+ result.SameSite = SameSiteMode.Strict;
+ }
+ else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
{
result.SameSite = SameSiteMode.Lax;
}
+ else if (!SuppressSameSiteNone
+ && StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
+ {
+ result.SameSite = SameSiteMode.None;
+ }
else
{
- result.SameSite = SameSiteMode.Strict;
+ result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.Unspecified;
}
}
}
diff --git a/src/Http/Headers/test/SetCookieHeaderValueTest.cs b/src/Http/Headers/test/SetCookieHeaderValueTest.cs
index 058f8d4bd972..11593b4f93c8 100644
--- a/src/Http/Headers/test/SetCookieHeaderValueTest.cs
+++ b/src/Http/Headers/test/SetCookieHeaderValueTest.cs
@@ -57,7 +57,7 @@ public static TheoryData SetCookieHeaderDataSet
{
SameSite = SameSiteMode.None,
};
- dataset.Add(header7, "name7=value7");
+ dataset.Add(header7, "name7=value7; samesite=none");
return dataset;
@@ -155,9 +155,20 @@ public static TheoryData, string[]> ListOfSetCookieH
{
SameSite = SameSiteMode.Strict
};
- var string6a = "name6=value6; samesite";
- var string6b = "name6=value6; samesite=Strict";
- var string6c = "name6=value6; samesite=invalid";
+ var string6 = "name6=value6; samesite=Strict";
+
+ var header7 = new SetCookieHeaderValue("name7", "value7")
+ {
+ SameSite = SameSiteMode.None
+ };
+ var string7 = "name7=value7; samesite=None";
+
+ var header8 = new SetCookieHeaderValue("name8", "value8")
+ {
+ SameSite = SameSiteMode.Unspecified
+ };
+ var string8a = "name8=value8; samesite";
+ var string8b = "name8=value8; samesite=invalid";
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
@@ -170,9 +181,10 @@ public static TheoryData, string[]> ListOfSetCookieH
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 });
+ dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
+ dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
+ dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
+ dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
return dataset;
}
@@ -301,6 +313,28 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp
Assert.Equal(expectedValue, input.ToString());
}
+ [Fact]
+ public void SetCookieHeaderValue_ToString_SameSiteNoneCompat()
+ {
+ SetCookieHeaderValue.SuppressSameSiteNone = true;
+
+ var input = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+
+ Assert.Equal("name=value", input.ToString());
+
+ SetCookieHeaderValue.SuppressSameSiteNone = false;
+
+ var input2 = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+
+ Assert.Equal("name=value; samesite=none", input2.ToString());
+ }
+
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
@@ -312,6 +346,32 @@ public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue inpu
Assert.Equal(expectedValue, builder.ToString());
}
+ [Fact]
+ public void SetCookieHeaderValue_AppendToStringBuilder_SameSiteNoneCompat()
+ {
+ SetCookieHeaderValue.SuppressSameSiteNone = true;
+
+ var builder = new StringBuilder();
+ var input = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+
+ input.AppendToStringBuilder(builder);
+ Assert.Equal("name=value", builder.ToString());
+
+ SetCookieHeaderValue.SuppressSameSiteNone = false;
+
+ var builder2 = new StringBuilder();
+ var input2 = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+
+ input2.AppendToStringBuilder(builder2);
+ Assert.Equal("name=value; samesite=none", builder2.ToString());
+ }
+
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -322,6 +382,31 @@ public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue c
Assert.Equal(expectedValue, header.ToString());
}
+ [Fact]
+ public void SetCookieHeaderValue_Parse_AcceptsValidValues_SameSiteNoneCompat()
+ {
+ SetCookieHeaderValue.SuppressSameSiteNone = true;
+ var header = SetCookieHeaderValue.Parse("name=value; samesite=none");
+
+ var cookie = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.Strict,
+ };
+
+ Assert.Equal(cookie, header);
+ Assert.Equal("name=value; samesite=strict", header.ToString());
+ SetCookieHeaderValue.SuppressSameSiteNone = false;
+
+ var header2 = SetCookieHeaderValue.Parse("name=value; samesite=none");
+
+ var cookie2 = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+ Assert.Equal(cookie2, header2);
+ Assert.Equal("name=value; samesite=none", header2.ToString());
+ }
+
[Theory]
[MemberData(nameof(SetCookieHeaderDataSet))]
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -332,6 +417,31 @@ public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValu
Assert.Equal(expectedValue, header.ToString());
}
+ [Fact]
+ public void SetCookieHeaderValue_TryParse_AcceptsValidValues_SameSiteNoneCompat()
+ {
+ SetCookieHeaderValue.SuppressSameSiteNone = true;
+ Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header));
+ var cookie = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.Strict,
+ };
+
+ Assert.Equal(cookie, header);
+ Assert.Equal("name=value; samesite=strict", header.ToString());
+
+ SetCookieHeaderValue.SuppressSameSiteNone = false;
+
+ Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header2));
+ var cookie2 = new SetCookieHeaderValue("name", "value")
+ {
+ SameSite = SameSiteMode.None,
+ };
+
+ Assert.Equal(cookie2, header2);
+ Assert.Equal("name=value; samesite=none", header2.ToString());
+ }
+
[Theory]
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
diff --git a/src/Http/Http.Abstractions/src/CookieBuilder.cs b/src/Http/Http.Abstractions/src/CookieBuilder.cs
index bbaaf07d1fd4..46ed33e7f820 100644
--- a/src/Http/Http.Abstractions/src/CookieBuilder.cs
+++ b/src/Http/Http.Abstractions/src/CookieBuilder.cs
@@ -11,8 +11,20 @@ namespace Microsoft.AspNetCore.Http
///
public class CookieBuilder
{
+ // True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
+ // False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
+ internal static bool SuppressSameSiteNone;
+
private string _name;
+ static CookieBuilder()
+ {
+ if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
+ {
+ SuppressSameSiteNone = enabled;
+ }
+ }
+
///
/// The name of the cookie.
///
@@ -49,12 +61,12 @@ public virtual string Name
public virtual bool HttpOnly { get; set; }
///
- /// The SameSite attribute of the cookie. The default value is
+ /// The SameSite attribute of the cookie. The default value is
///
///
/// Determines the value that will set on .
///
- public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.None;
+ public virtual SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
///
/// The policy that will be used to determine .
diff --git a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netcoreapp.cs b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netcoreapp.cs
index a7327d08fc8f..0ba855126006 100644
--- a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netcoreapp.cs
+++ b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netcoreapp.cs
@@ -84,6 +84,7 @@ public partial interface ISession
}
public enum SameSiteMode
{
+ Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,
diff --git a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs
index a7327d08fc8f..0ba855126006 100644
--- a/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs
+++ b/src/Http/Http.Features/ref/Microsoft.AspNetCore.Http.Features.netstandard2.0.cs
@@ -84,6 +84,7 @@ public partial interface ISession
}
public enum SameSiteMode
{
+ Unspecified = -1,
None = 0,
Lax = 1,
Strict = 2,
diff --git a/src/Http/Http.Features/src/CookieOptions.cs b/src/Http/Http.Features/src/CookieOptions.cs
index 81e883bd5615..6720ad6f0525 100644
--- a/src/Http/Http.Features/src/CookieOptions.cs
+++ b/src/Http/Http.Features/src/CookieOptions.cs
@@ -10,6 +10,18 @@ namespace Microsoft.AspNetCore.Http
///
public class CookieOptions
{
+ // True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
+ // False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
+ internal static bool SuppressSameSiteNone;
+
+ static CookieOptions()
+ {
+ if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
+ {
+ SuppressSameSiteNone = enabled;
+ }
+ }
+
///
/// Creates a default cookie with a path of '/'.
///
@@ -43,10 +55,10 @@ public CookieOptions()
public bool Secure { get; set; }
///
- /// Gets or sets the value for the SameSite attribute of the cookie. The default value is
+ /// 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.None;
+ public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
///
/// Gets or sets a value that indicates whether a cookie is accessible by client-side script.
diff --git a/src/Http/Http.Features/src/SameSiteMode.cs b/src/Http/Http.Features/src/SameSiteMode.cs
index d1af765a93d6..c9a11143976b 100644
--- a/src/Http/Http.Features/src/SameSiteMode.cs
+++ b/src/Http/Http.Features/src/SameSiteMode.cs
@@ -5,12 +5,14 @@ namespace Microsoft.AspNetCore.Http
{
///
/// Used to set the SameSite field on response cookies to indicate if those cookies should be included by the client on future "same-site" or "cross-site" requests.
- /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
+ /// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
///
// This mirrors Microsoft.Net.Http.Headers.SameSiteMode
public enum SameSiteMode
{
/// No SameSite field will be set, the client should follow its default cookie policy.
+ Unspecified = -1,
+ /// Indicates the client should disable same-site restrictions.
None = 0,
/// Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.
Lax,
diff --git a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/OpenIdConnectSample.csproj b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/OpenIdConnectSample.csproj
index 5f5dbaccc2c0..e416aedf54b7 100644
--- a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/OpenIdConnectSample.csproj
+++ b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/OpenIdConnectSample.csproj
@@ -1,4 +1,4 @@
-
+
$(DefaultNetCoreTargetFramework)
@@ -14,6 +14,7 @@
+
diff --git a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
index b30f479b8cd7..f26e23a8e541 100644
--- a/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
+++ b/src/Security/Authentication/OpenIdConnect/samples/OpenIdConnectSample/Startup.cs
@@ -32,14 +32,36 @@ public Startup(IConfiguration config, IWebHostEnvironment env)
public IConfiguration Configuration { get; set; }
public IWebHostEnvironment Environment { get; }
+ private void CheckSameSite(HttpContext httpContext, CookieOptions options)
+ {
+ if (options.SameSite > SameSiteMode.Unspecified)
+ {
+ var userAgent = httpContext.Request.Headers["User-Agent"];
+ // TODO: Use your User Agent library of choice here.
+ if (userAgent.Contains("CPU iPhone OS 12") // Also covers iPod touch
+ || userAgent.Contains("iPad; CPU OS 12")
+ // Safari 12 and 13 are both broken on Mojave
+ || userAgent.Contains("Macintosh; Intel Mac OS X 10_14"))
+ {
+ options.SameSite = SameSiteMode.Unspecified;
+ }
+ }
+ }
+
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
+ services.Configure(options =>
+ {
+ options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
+ options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
+ options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
+ });
+
services.AddAuthentication(sharedOptions =>
{
- sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
- sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
@@ -84,6 +106,7 @@ public void ConfigureServices(IServiceCollection services)
public void Configure(IApplicationBuilder app, IOptionsMonitor optionsMonitor)
{
app.UseDeveloperExceptionPage();
+ app.UseCookiePolicy(); // Before UseAuthentication or anything else that writes cookies.
app.UseAuthentication();
app.Run(async context =>
diff --git a/src/Security/Authentication/WsFederation/samples/WsFedSample/WsFedSample.csproj b/src/Security/Authentication/WsFederation/samples/WsFedSample/WsFedSample.csproj
index 0d6734f909cb..dbaba2adafc4 100644
--- a/src/Security/Authentication/WsFederation/samples/WsFedSample/WsFedSample.csproj
+++ b/src/Security/Authentication/WsFederation/samples/WsFedSample/WsFedSample.csproj
@@ -2,6 +2,7 @@
$(DefaultNetCoreTargetFramework)
+ OutOfProcess
diff --git a/src/Security/Authentication/test/CookieTests.cs b/src/Security/Authentication/test/CookieTests.cs
index 439c2cccc42c..44775187129f 100644
--- a/src/Security/Authentication/test/CookieTests.cs
+++ b/src/Security/Authentication/test/CookieTests.cs
@@ -229,7 +229,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
Assert.Contains(" path=/foo", setCookie1);
Assert.Contains(" domain=another.com", setCookie1);
Assert.Contains(" secure", setCookie1);
- Assert.DoesNotContain(" samesite", setCookie1);
+ Assert.Contains(" samesite=none", setCookie1);
Assert.Contains(" httponly", setCookie1);
var server2 = CreateServer(o =>
diff --git a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs
index d6836591182b..aeb6c91b7f56 100644
--- a/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs
+++ b/src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs
@@ -437,6 +437,7 @@ public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavio
var server = settings.CreateTestServer();
var transaction = await server.SendAsync(ChallengeEndpoint);
+ Assert.Contains("samesite=none", transaction.SetCookie.First());
var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
Assert.True(nonceCookie.Expires.HasValue);
@@ -452,6 +453,7 @@ public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavio
Assert.True(correlationCookie.HttpOnly);
Assert.Equal("/signin-oidc", correlationCookie.Path);
Assert.False(StringSegment.IsNullOrEmpty(correlationCookie.Value));
+ Assert.Equal(Net.Http.Headers.SameSiteMode.None, correlationCookie.SameSite);
Assert.Equal(2, challengeCookies.Count);
}
diff --git a/src/Security/CookiePolicy/src/CookiePolicyOptions.cs b/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
index 4f0806c46c95..098bd3348395 100644
--- a/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
+++ b/src/Security/CookiePolicy/src/CookiePolicyOptions.cs
@@ -12,10 +12,22 @@ namespace Microsoft.AspNetCore.Builder
///
public class CookiePolicyOptions
{
+ // True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
+ // False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
+ internal static bool SuppressSameSiteNone;
+
+ static CookiePolicyOptions()
+ {
+ if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
+ {
+ SuppressSameSiteNone = enabled;
+ }
+ }
+
///
/// Affects the cookie's same site attribute.
///
- public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.None;
+ public SameSiteMode MinimumSameSitePolicy { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
///
/// Affects whether cookies must be HttpOnly.
diff --git a/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs b/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
index 126c4d7bd525..879df47018fb 100644
--- a/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
+++ b/src/Security/CookiePolicy/src/ResponseCookiesWrapper.cs
@@ -1,4 +1,4 @@
-// Copyright (c) .NET Foundation. All rights reserved.
+// 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.
using System;
@@ -115,7 +115,8 @@ public string CreateConsentCookie()
private bool CheckPolicyRequired()
{
return !CanTrack
- || Options.MinimumSameSitePolicy != SameSiteMode.None
+ || (CookiePolicyOptions.SuppressSameSiteNone && Options.MinimumSameSitePolicy != SameSiteMode.None)
+ || (!CookiePolicyOptions.SuppressSameSiteNone && Options.MinimumSameSitePolicy != SameSiteMode.Unspecified)
|| Options.HttpOnly != HttpOnlyPolicy.None
|| Options.Secure != CookieSecurePolicy.None;
}
@@ -241,26 +242,10 @@ private void ApplyPolicy(string key, CookieOptions options)
default:
throw new InvalidOperationException();
}
- switch (Options.MinimumSameSitePolicy)
+ if (options.SameSite < Options.MinimumSameSitePolicy)
{
- case SameSiteMode.None:
- break;
- case SameSiteMode.Lax:
- if (options.SameSite == SameSiteMode.None)
- {
- options.SameSite = SameSiteMode.Lax;
- _logger.CookieSameSiteUpgraded(key, "lax");
- }
- break;
- case SameSiteMode.Strict:
- if (options.SameSite != SameSiteMode.Strict)
- {
- options.SameSite = SameSiteMode.Strict;
- _logger.CookieSameSiteUpgraded(key, "strict");
- }
- break;
- default:
- throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
+ options.SameSite = Options.MinimumSameSitePolicy;
+ _logger.CookieSameSiteUpgraded(key, Options.MinimumSameSitePolicy.ToString());
}
switch (Options.HttpOnly)
{
@@ -278,4 +263,4 @@ private void ApplyPolicy(string key, CookieOptions options)
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Security/CookiePolicy/test/CookieConsentTests.cs b/src/Security/CookiePolicy/test/CookieConsentTests.cs
index ffe8c30619ea..cda7e7d93c3f 100644
--- a/src/Security/CookiePolicy/test/CookieConsentTests.cs
+++ b/src/Security/CookiePolicy/test/CookieConsentTests.cs
@@ -223,12 +223,12 @@ public async Task GrantConsentSetsCookie()
Assert.Equal("yes", consentCookie.Value);
Assert.True(consentCookie.Expires.HasValue);
Assert.True(consentCookie.Expires.Value > DateTimeOffset.Now + TimeSpan.FromDays(364));
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, consentCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
var testCookie = cookies[1];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("Value", testCookie.Value);
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, testCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
Assert.Null(testCookie.Expires);
}
@@ -400,12 +400,12 @@ public async Task WithdrawConsentDeletesCookie()
var testCookie = cookies[0];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("Value1", testCookie.Value);
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, testCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
Assert.Null(testCookie.Expires);
var consentCookie = cookies[1];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("", consentCookie.Value);
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, consentCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
}
@@ -512,7 +512,7 @@ public async Task DeleteCookieDoesNotRequireConsent()
var testCookie = cookies[0];
Assert.Equal("Test", testCookie.Name);
Assert.Equal("", testCookie.Value);
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, testCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, testCookie.SameSite);
Assert.NotNull(testCookie.Expires);
}
@@ -576,7 +576,7 @@ public async Task CreateConsentCookieMatchesGrantConsentCookie()
var consentCookie = cookies[0];
Assert.Equal(".AspNet.Consent", consentCookie.Name);
Assert.Equal("yes", consentCookie.Value);
- Assert.Equal(Net.Http.Headers.SameSiteMode.None, consentCookie.SameSite);
+ Assert.Equal(Net.Http.Headers.SameSiteMode.Unspecified, consentCookie.SameSite);
Assert.NotNull(consentCookie.Expires);
cookies = SetCookieHeaderValue.ParseList(httpContext.Response.Headers["ManualCookie"]);
diff --git a/src/Security/CookiePolicy/test/CookiePolicyTests.cs b/src/Security/CookiePolicy/test/CookiePolicyTests.cs
index cf233360fa6d..395d328f1d6e 100644
--- a/src/Security/CookiePolicy/test/CookiePolicyTests.cs
+++ b/src/Security/CookiePolicy/test/CookiePolicyTests.cs
@@ -39,8 +39,8 @@ public class CookiePolicyTests
private RequestDelegate SameSiteCookieAppends = context =>
{
context.Response.Cookies.Append("A", "A");
- context.Response.Cookies.Append("B", "B", new CookieOptions { SameSite = Http.SameSiteMode.None });
- context.Response.Cookies.Append("C", "C", new CookieOptions());
+ context.Response.Cookies.Append("B", "B", new CookieOptions());
+ context.Response.Cookies.Append("C", "C", new CookieOptions { SameSite = Http.SameSiteMode.None });
context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
return Task.FromResult(0);
@@ -198,7 +198,7 @@ await RunTest("/sameSiteLax",
}
[Fact]
- public async Task SameSiteNoneLeavesItAlone()
+ public async Task SameSiteNoneSetsItAlways()
{
await RunTest("/sameSiteNone",
new CookiePolicyOptions
@@ -208,11 +208,32 @@ await RunTest("/sameSiteNone",
SameSiteCookieAppends,
new RequestTest("http://example.com/sameSiteNone",
transaction =>
+ {
+ Assert.NotNull(transaction.SetCookie);
+ Assert.Equal("A=A; path=/; samesite=none", transaction.SetCookie[0]);
+ Assert.Equal("B=B; path=/; samesite=none", transaction.SetCookie[1]);
+ Assert.Equal("C=C; path=/; samesite=none", transaction.SetCookie[2]);
+ Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
+ Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
+ }));
+ }
+
+ [Fact]
+ public async Task SameSiteUnspecifiedLeavesItAlone()
+ {
+ await RunTest("/sameSiteNone",
+ new CookiePolicyOptions
+ {
+ MinimumSameSitePolicy = Http.SameSiteMode.Unspecified
+ },
+ SameSiteCookieAppends,
+ new RequestTest("http://example.com/sameSiteNone",
+ transaction =>
{
Assert.NotNull(transaction.SetCookie);
Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
- Assert.Equal("C=C; path=/", transaction.SetCookie[2]);
+ Assert.Equal("C=C; path=/; samesite=none", transaction.SetCookie[2]);
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
}));