[2.1] Re-implement SameSite for 2019 (#13746)
* Re-implement SameSite for 2019 #12125 * Rename compat flag * Use Microsoft.AspNetCore.SuppressSameSiteNone compat key * Backport CookiePolicy quirk and sample * Patch config
This commit is contained in:
parent
a9fb7f4a6d
commit
f198e559f8
|
|
@ -40,6 +40,8 @@ Later on, this will be checked using this condition:
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.14' ">
|
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.1.14' ">
|
||||||
<PackagesInPatch>
|
<PackagesInPatch>
|
||||||
|
Microsoft.Net.Http.Headers;
|
||||||
|
Microsoft.AspNetCore.CookiePolicy;
|
||||||
</PackagesInPatch>
|
</PackagesInPatch>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,14 @@ namespace Microsoft.Net.Http.Headers
|
||||||
private const string SecureToken = "secure";
|
private const string SecureToken = "secure";
|
||||||
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
|
||||||
private const string SameSiteToken = "samesite";
|
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 SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
|
||||||
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.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 HttpOnlyToken = "httponly";
|
||||||
private const string SeparatorToken = "; ";
|
private const string SeparatorToken = "; ";
|
||||||
private const string EqualsToken = "=";
|
private const string EqualsToken = "=";
|
||||||
|
|
@ -34,6 +40,14 @@ namespace Microsoft.Net.Http.Headers
|
||||||
private StringSegment _name;
|
private StringSegment _name;
|
||||||
private StringSegment _value;
|
private StringSegment _value;
|
||||||
|
|
||||||
|
static SetCookieHeaderValue()
|
||||||
|
{
|
||||||
|
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
|
||||||
|
{
|
||||||
|
SuppressSameSiteNone = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SetCookieHeaderValue()
|
private SetCookieHeaderValue()
|
||||||
{
|
{
|
||||||
// Used by the parser to create a new instance of this type.
|
// Used by the parser to create a new instance of this type.
|
||||||
|
|
@ -90,11 +104,11 @@ namespace Microsoft.Net.Http.Headers
|
||||||
|
|
||||||
public bool Secure { get; set; }
|
public bool Secure { get; set; }
|
||||||
|
|
||||||
public SameSiteMode SameSite { get; set; }
|
public SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : (SameSiteMode)(-1); // Unspecified
|
||||||
|
|
||||||
public bool HttpOnly { get; set; }
|
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()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var length = _name.Length + EqualsToken.Length + _value.Length;
|
var length = _name.Length + EqualsToken.Length + _value.Length;
|
||||||
|
|
@ -130,9 +144,20 @@ namespace Microsoft.Net.Http.Headers
|
||||||
length += SeparatorToken.Length + SecureToken.Length;
|
length += SeparatorToken.Length + SecureToken.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SameSite != SameSiteMode.None)
|
// Allow for Unspecified (-1) to skip SameSite
|
||||||
|
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
|
||||||
{
|
{
|
||||||
sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
|
sameSite = SameSiteNoneToken;
|
||||||
|
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||||
|
}
|
||||||
|
else if (SameSite == SameSiteMode.Lax)
|
||||||
|
{
|
||||||
|
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;
|
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,7 +197,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
AppendSegment(ref sb, SecureToken, null);
|
AppendSegment(ref sb, SecureToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SameSite != SameSiteMode.None)
|
if (sameSite != null)
|
||||||
{
|
{
|
||||||
AppendSegment(ref sb, SameSiteToken, sameSite);
|
AppendSegment(ref sb, SameSiteToken, sameSite);
|
||||||
}
|
}
|
||||||
|
|
@ -235,9 +260,18 @@ namespace Microsoft.Net.Http.Headers
|
||||||
AppendSegment(builder, SecureToken, null);
|
AppendSegment(builder, SecureToken, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SameSite != SameSiteMode.None)
|
// Allow for Unspecified (-1) to skip SameSite
|
||||||
|
if (SameSite == SameSiteMode.None && !SuppressSameSiteNone)
|
||||||
{
|
{
|
||||||
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
|
AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
|
||||||
|
}
|
||||||
|
else if (SameSite == SameSiteMode.Lax)
|
||||||
|
{
|
||||||
|
AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
|
||||||
|
}
|
||||||
|
else if (SameSite == SameSiteMode.Strict)
|
||||||
|
{
|
||||||
|
AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HttpOnly)
|
if (HttpOnly)
|
||||||
|
|
@ -289,7 +323,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
|
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; 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
|
||||||
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
|
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
|
||||||
{
|
{
|
||||||
Contract.Requires(startIndex >= 0);
|
Contract.Requires(startIndex >= 0);
|
||||||
|
|
@ -424,25 +458,34 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
result.Secure = true;
|
result.Secure = true;
|
||||||
}
|
}
|
||||||
// samesite-av = "SameSite" / "SameSite=" samesite-value
|
// samesite-av = "SameSite=" samesite-value
|
||||||
// samesite-value = "Strict" / "Lax"
|
// samesite-value = "Strict" / "Lax" / "None"
|
||||||
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
|
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
if (!ReadEqualsSign(input, ref offset))
|
if (!ReadEqualsSign(input, ref offset))
|
||||||
{
|
{
|
||||||
result.SameSite = SameSiteMode.Strict;
|
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
|
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;
|
result.SameSite = SameSiteMode.Lax;
|
||||||
}
|
}
|
||||||
|
else if (!SuppressSameSiteNone
|
||||||
|
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.SameSite = SameSiteMode.None;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.SameSite = SameSiteMode.Strict;
|
result.SameSite = SuppressSameSiteNone ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
SameSite = SameSiteMode.None,
|
SameSite = SameSiteMode.None,
|
||||||
};
|
};
|
||||||
dataset.Add(header7, "name7=value7");
|
dataset.Add(header7, "name7=value7; samesite=none");
|
||||||
|
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
|
|
@ -155,9 +155,20 @@ namespace Microsoft.Net.Http.Headers
|
||||||
{
|
{
|
||||||
SameSite = SameSiteMode.Strict
|
SameSite = SameSiteMode.Strict
|
||||||
};
|
};
|
||||||
var string6a = "name6=value6; samesite";
|
var string6 = "name6=value6; samesite=Strict";
|
||||||
var string6b = "name6=value6; samesite=Strict";
|
|
||||||
var string6c = "name6=value6; samesite=invalid";
|
var header7 = new SetCookieHeaderValue("name7", "value7")
|
||||||
|
{
|
||||||
|
SameSite = SameSiteMode.None
|
||||||
|
};
|
||||||
|
var string7 = "name7=value7; samesite=None";
|
||||||
|
|
||||||
|
var header8 = new SetCookieHeaderValue("name8", "value8")
|
||||||
|
{
|
||||||
|
SameSite = (SameSiteMode)(-1) // Unspecified
|
||||||
|
};
|
||||||
|
var string8a = "name8=value8; samesite";
|
||||||
|
var string8b = "name8=value8; samesite=invalid";
|
||||||
|
|
||||||
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
|
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
|
||||||
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
|
||||||
|
|
@ -170,9 +181,10 @@ namespace Microsoft.Net.Http.Headers
|
||||||
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", 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[] { string5a });
|
||||||
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
|
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
|
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
|
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
|
||||||
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
|
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
|
||||||
|
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
|
||||||
|
|
||||||
return dataset;
|
return dataset;
|
||||||
}
|
}
|
||||||
|
|
@ -301,6 +313,28 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, input.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
|
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
|
||||||
|
|
@ -312,6 +346,32 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, builder.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||||
|
|
@ -322,6 +382,31 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, header.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(SetCookieHeaderDataSet))]
|
[MemberData(nameof(SetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
|
||||||
|
|
@ -332,6 +417,31 @@ namespace Microsoft.Net.Http.Headers
|
||||||
Assert.Equal(expectedValue, header.ToString());
|
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]
|
[Theory]
|
||||||
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
|
||||||
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
|
<TargetFrameworks>net461;netcoreapp2.1</TargetFrameworks>
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
<Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" />
|
||||||
|
<Reference Include="Microsoft.AspNetCore.CookiePolicy" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
<Reference Include="Microsoft.AspNetCore.Server.Kestrel.Https" />
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
<Reference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
||||||
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
<Reference Include="Microsoft.Extensions.Logging.Console" />
|
||||||
<Reference Include="Microsoft.Extensions.Logging.Debug" />
|
<Reference Include="Microsoft.Extensions.Logging.Debug" />
|
||||||
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,33 @@ namespace OpenIdConnectSample
|
||||||
|
|
||||||
public IHostingEnvironment Environment { get; set; }
|
public IHostingEnvironment Environment { get; set; }
|
||||||
|
|
||||||
|
private void CheckSameSite(HttpContext httpContext, CookieOptions options)
|
||||||
|
{
|
||||||
|
if (options.SameSite > (SameSiteMode)(-1))
|
||||||
|
{
|
||||||
|
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
|
||||||
|
// 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)(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
|
||||||
|
|
||||||
|
services.Configure<CookiePolicyOptions>(options =>
|
||||||
|
{
|
||||||
|
options.MinimumSameSitePolicy = (SameSiteMode)(-1);
|
||||||
|
options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
|
||||||
|
options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
|
||||||
|
});
|
||||||
|
|
||||||
services.AddAuthentication(sharedOptions =>
|
services.AddAuthentication(sharedOptions =>
|
||||||
{
|
{
|
||||||
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||||
|
|
@ -56,9 +79,15 @@ namespace OpenIdConnectSample
|
||||||
.AddCookie()
|
.AddCookie()
|
||||||
.AddOpenIdConnect(o =>
|
.AddOpenIdConnect(o =>
|
||||||
{
|
{
|
||||||
|
/*
|
||||||
o.ClientId = Configuration["oidc:clientid"];
|
o.ClientId = Configuration["oidc:clientid"];
|
||||||
o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow
|
o.ClientSecret = Configuration["oidc:clientsecret"]; // for code flow
|
||||||
o.Authority = Configuration["oidc:authority"];
|
o.Authority = Configuration["oidc:authority"];
|
||||||
|
*/
|
||||||
|
// https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs
|
||||||
|
o.ClientId = "server.hybrid";
|
||||||
|
o.ClientSecret = "secret"; // for code flow
|
||||||
|
o.Authority = "https://demo.identityserver.io/";
|
||||||
|
|
||||||
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
|
o.ResponseType = OpenIdConnectResponseType.CodeIdToken;
|
||||||
o.SaveTokens = true;
|
o.SaveTokens = true;
|
||||||
|
|
@ -88,6 +117,7 @@ namespace OpenIdConnectSample
|
||||||
public void Configure(IApplicationBuilder app, IOptionsMonitor<OpenIdConnectOptions> optionsMonitor)
|
public void Configure(IApplicationBuilder app, IOptionsMonitor<OpenIdConnectOptions> optionsMonitor)
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
app.UseCookiePolicy(); // Before UseAuthentication or anything else that writes cookies.
|
||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
|
|
||||||
app.Run(async context =>
|
app.Run(async context =>
|
||||||
|
|
|
||||||
|
|
@ -649,7 +649,7 @@ namespace Microsoft.AspNetCore.Authentication.Cookies
|
||||||
Assert.Contains(" path=/foo", setCookie1);
|
Assert.Contains(" path=/foo", setCookie1);
|
||||||
Assert.Contains(" domain=another.com", setCookie1);
|
Assert.Contains(" domain=another.com", setCookie1);
|
||||||
Assert.Contains(" secure", setCookie1);
|
Assert.Contains(" secure", setCookie1);
|
||||||
Assert.DoesNotContain(" samesite", setCookie1);
|
Assert.Contains(" samesite=none", setCookie1);
|
||||||
Assert.Contains(" httponly", setCookie1);
|
Assert.Contains(" httponly", setCookie1);
|
||||||
|
|
||||||
var server2 = CreateServer(o =>
|
var server2 = CreateServer(o =>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
|
<Reference Include="Microsoft.AspNetCore.Authentication.Twitter" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" />
|
<Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" />
|
||||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||||
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,7 @@ namespace Microsoft.AspNetCore.Authentication.Test.OpenIdConnect
|
||||||
var server = settings.CreateTestServer();
|
var server = settings.CreateTestServer();
|
||||||
var transaction = await server.SendAsync(ChallengeEndpoint);
|
var transaction = await server.SendAsync(ChallengeEndpoint);
|
||||||
|
|
||||||
|
Assert.Contains("samesite=none", transaction.SetCookie.First());
|
||||||
var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
|
var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
|
||||||
var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
|
var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
|
||||||
Assert.True(nonceCookie.Expires.HasValue);
|
Assert.True(nonceCookie.Expires.HasValue);
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,18 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CookiePolicyOptions
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Affects the cookie's same site attribute.
|
/// Affects the cookie's same site attribute.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
@ -115,7 +115,8 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
private bool CheckPolicyRequired()
|
private bool CheckPolicyRequired()
|
||||||
{
|
{
|
||||||
return !CanTrack
|
return !CanTrack
|
||||||
|| Options.MinimumSameSitePolicy != SameSiteMode.None
|
|| (CookiePolicyOptions.SuppressSameSiteNone && Options.MinimumSameSitePolicy != SameSiteMode.None)
|
||||||
|
|| (!CookiePolicyOptions.SuppressSameSiteNone && Options.MinimumSameSitePolicy != (SameSiteMode)(-1))
|
||||||
|| Options.HttpOnly != HttpOnlyPolicy.None
|
|| Options.HttpOnly != HttpOnlyPolicy.None
|
||||||
|| Options.Secure != CookieSecurePolicy.None;
|
|| Options.Secure != CookieSecurePolicy.None;
|
||||||
}
|
}
|
||||||
|
|
@ -241,26 +242,10 @@ namespace Microsoft.AspNetCore.CookiePolicy
|
||||||
default:
|
default:
|
||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
switch (Options.MinimumSameSitePolicy)
|
if (options.SameSite < Options.MinimumSameSitePolicy)
|
||||||
{
|
{
|
||||||
case SameSiteMode.None:
|
options.SameSite = Options.MinimumSameSitePolicy;
|
||||||
break;
|
_logger.CookieSameSiteUpgraded(key, Options.MinimumSameSitePolicy.ToString());
|
||||||
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()}");
|
|
||||||
}
|
}
|
||||||
switch (Options.HttpOnly)
|
switch (Options.HttpOnly)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
|
||||||
context.Response.Cookies.Append("C", "C", new CookieOptions());
|
context.Response.Cookies.Append("C", "C", new CookieOptions());
|
||||||
context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
|
context.Response.Cookies.Append("D", "D", new CookieOptions { SameSite = Http.SameSiteMode.Lax });
|
||||||
context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
|
context.Response.Cookies.Append("E", "E", new CookieOptions { SameSite = Http.SameSiteMode.Strict });
|
||||||
|
context.Response.Cookies.Append("F", "F", new CookieOptions { SameSite = (Http.SameSiteMode)(-1) });
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -198,7 +199,7 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SameSiteNoneLeavesItAlone()
|
public async Task SameSiteNoneSetsItAlways()
|
||||||
{
|
{
|
||||||
await RunTest("/sameSiteNone",
|
await RunTest("/sameSiteNone",
|
||||||
new CookiePolicyOptions
|
new CookiePolicyOptions
|
||||||
|
|
@ -210,11 +211,34 @@ namespace Microsoft.AspNetCore.CookiePolicy.Test
|
||||||
transaction =>
|
transaction =>
|
||||||
{
|
{
|
||||||
Assert.NotNull(transaction.SetCookie);
|
Assert.NotNull(transaction.SetCookie);
|
||||||
Assert.Equal("A=A; path=/", transaction.SetCookie[0]);
|
Assert.Equal("A=A; path=/; samesite=lax", transaction.SetCookie[0]);
|
||||||
Assert.Equal("B=B; path=/", transaction.SetCookie[1]);
|
Assert.Equal("B=B; path=/; samesite=none", transaction.SetCookie[1]);
|
||||||
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
|
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
|
||||||
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
|
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
|
||||||
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
|
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
|
||||||
|
Assert.Equal("F=F; path=/; samesite=none", transaction.SetCookie[5]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SameSiteUnspecifiedLeavesItAlone()
|
||||||
|
{
|
||||||
|
await RunTest("/sameSiteNone",
|
||||||
|
new CookiePolicyOptions
|
||||||
|
{
|
||||||
|
MinimumSameSitePolicy = (Http.SameSiteMode)(-1)
|
||||||
|
},
|
||||||
|
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=/; samesite=none", transaction.SetCookie[1]);
|
||||||
|
Assert.Equal("C=C; path=/; samesite=lax", transaction.SetCookie[2]);
|
||||||
|
Assert.Equal("D=D; path=/; samesite=lax", transaction.SetCookie[3]);
|
||||||
|
Assert.Equal("E=E; path=/; samesite=strict", transaction.SetCookie[4]);
|
||||||
|
Assert.Equal("F=F; path=/", transaction.SetCookie[5]);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
<TargetFrameworks>netcoreapp2.1;net461</TargetFrameworks>
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.CookiePolicy" />
|
<Reference Include="Microsoft.AspNetCore.CookiePolicy" />
|
||||||
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
<Reference Include="Microsoft.AspNetCore.TestHost" />
|
||||||
<Reference Include="Microsoft.Extensions.DependencyInjection" />
|
<Reference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
<Reference Include="Microsoft.Net.Http.Headers" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "..\Servers\IIS\IISIntegration\src\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.IISIntegration", "..\Servers\IIS\IISIntegration\src\Microsoft.AspNetCore.Server.IISIntegration.csproj", "{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Net.Http.Headers", "..\Http\Headers\src\Microsoft.Net.Http.Headers.csproj", "{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -248,6 +250,10 @@ Global
|
||||||
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Release|Any CPU.Build.0 = Release|Any CPU
|
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -298,6 +304,7 @@ Global
|
||||||
{707CBFB4-4D35-479E-9BAF-39B4DA9782DE} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
{707CBFB4-4D35-479E-9BAF-39B4DA9782DE} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
||||||
{AFE880E8-2E9E-46FD-BE87-DFC8192E7B2D} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
{AFE880E8-2E9E-46FD-BE87-DFC8192E7B2D} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
||||||
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
{81D0E81F-4711-4C7B-BBD4-E168102D0D7D} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
||||||
|
{4BB8D7D7-E111-4A86-B6E5-C1201E0DA8CE} = {A3766414-EB5C-40F7-B031-121804ED5D0A}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
|
SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue