diff --git a/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs b/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs
index b88dba14..55c04f5a 100644
--- a/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs
+++ b/src/Microsoft.Owin.Host.SystemWeb/SystemWebChunkingCookieManager.cs
@@ -163,6 +163,7 @@ public void AppendResponseCookie(IOwinContext context, string key, string value,
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
+ bool sameSiteHasValue = options.SameSite.HasValue && SystemWebCookieManager.IsSameSiteAvailable;
string escapedKey = Uri.EscapeDataString(key);
string prefix = escapedKey + "=";
@@ -173,9 +174,12 @@ public void AppendResponseCookie(IOwinContext context, string key, string value,
!pathHasValue ? null : "; path=",
!pathHasValue ? null : options.Path,
!expiresHasValue ? null : "; expires=",
- !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT",
+ !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss \\G\\M\\T", CultureInfo.InvariantCulture),
!options.Secure ? null : "; secure",
- !options.HttpOnly ? null : "; HttpOnly");
+ !options.HttpOnly ? null : "; HttpOnly",
+ !sameSiteHasValue ? null : "; SameSite=",
+ !sameSiteHasValue ? null : GetStringRepresentationOfSameSite(options.SameSite.Value)
+ );
value = value ?? string.Empty;
bool quoted = false;
@@ -273,6 +277,9 @@ public void DeleteCookie(IOwinContext context, string key, CookieOptions options
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ SameSite = options.SameSite,
+ Secure = options.Secure,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
@@ -286,6 +293,9 @@ public void DeleteCookie(IOwinContext context, string key, CookieOptions options
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ SameSite = options.SameSite,
+ Secure = options.Secure,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
@@ -313,6 +323,14 @@ private static void SetOptions(HttpCookie cookie, CookieOptions options, bool do
{
cookie.HttpOnly = true;
}
+
+ if (SystemWebCookieManager.IsSameSiteAvailable)
+ {
+ SystemWebCookieManager.SameSiteSetter.Invoke(cookie, new object[]
+ {
+ options.SameSite ?? (SameSiteMode)(-1) // Unspecified
+ });
+ }
}
private static bool IsQuoted(string value)
@@ -329,5 +347,21 @@ private static string Quote(string value)
{
return '"' + value + '"';
}
+
+ private static string GetStringRepresentationOfSameSite(SameSiteMode siteMode)
+ {
+ switch (siteMode)
+ {
+ case SameSiteMode.None:
+ return "None";
+ case SameSiteMode.Lax:
+ return "Lax";
+ case SameSiteMode.Strict:
+ return "Strict";
+ default:
+ throw new ArgumentOutOfRangeException("siteMode",
+ string.Format(CultureInfo.InvariantCulture, "Unexpected SameSiteMode value: {0}", siteMode));
+ }
+ }
}
}
diff --git a/src/Microsoft.Owin.Host.SystemWeb/SystemWebCookieManager.cs b/src/Microsoft.Owin.Host.SystemWeb/SystemWebCookieManager.cs
index 5eb3c8aa..8a1fcc82 100644
--- a/src/Microsoft.Owin.Host.SystemWeb/SystemWebCookieManager.cs
+++ b/src/Microsoft.Owin.Host.SystemWeb/SystemWebCookieManager.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Reflection;
using System.Web;
using Microsoft.Owin.Infrastructure;
@@ -12,6 +13,21 @@ namespace Microsoft.Owin.Host.SystemWeb
///
public class SystemWebCookieManager : ICookieManager
{
+ // .NET 4.7.2, but requries a patch to emit SameSite=None
+ internal static readonly bool IsSameSiteAvailable;
+ internal static readonly MethodInfo SameSiteSetter;
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline")]
+ static SystemWebCookieManager()
+ {
+ var systemWeb = typeof(HttpContextBase).Assembly;
+ IsSameSiteAvailable = systemWeb.GetType("System.Web.SameSiteMode") != null;
+ if (IsSameSiteAvailable)
+ {
+ SameSiteSetter = typeof(HttpCookie).GetProperty("SameSite").SetMethod;
+ }
+ }
+
///
/// Creates a new instance of SystemWebCookieManager.
///
@@ -104,6 +120,13 @@ public void AppendResponseCookie(IOwinContext context, string key, string value,
{
cookie.HttpOnly = true;
}
+ if (IsSameSiteAvailable)
+ {
+ SameSiteSetter.Invoke(cookie, new object[]
+ {
+ options.SameSite ?? (SameSiteMode)(-1) // Unspecified
+ });
+ }
webContext.Response.AppendCookie(cookie);
}
@@ -133,6 +156,9 @@ public void DeleteCookie(IOwinContext context, string key, CookieOptions options
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ Secure = options.Secure,
+ SameSite = options.SameSite,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
diff --git a/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs
index 743368e3..25dc2567 100644
--- a/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs
+++ b/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationHandler.cs
@@ -146,6 +146,7 @@ protected override async Task ApplyResponseGrantAsync()
{
Domain = Options.CookieDomain,
HttpOnly = Options.CookieHttpOnly,
+ SameSite = Options.CookieSameSite,
Path = Options.CookiePath ?? "/",
};
if (Options.CookieSecure == CookieSecureOption.SameAsRequest)
diff --git a/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationOptions.cs
index 9d8d58d0..019209f7 100644
--- a/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationOptions.cs
+++ b/src/Microsoft.Owin.Security.Cookies/CookieAuthenticationOptions.cs
@@ -65,6 +65,12 @@ public string CookieName
///
public bool CookieHttpOnly { get; set; }
+ ///
+ /// Determines if the browser should allow the cookie to be sent with requests initiated from other sites.
+ /// The default is 'null' to exclude the setting and let the browser choose the default behavior.
+ ///
+ public SameSiteMode? CookieSameSite { get; set; }
+
///
/// Determines if the cookie should only be transmitted on HTTPS request. The default is to limit the cookie
/// to HTTPS requests if the page which is doing the SignIn is also HTTPS. If you have an HTTPS sign in page
diff --git a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs
index bee807bd..7a207e29 100644
--- a/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs
+++ b/src/Microsoft.Owin.Security.OpenIdConnect/OpenidConnectAuthenticationHandler.cs
@@ -637,6 +637,7 @@ protected virtual void RememberNonce(OpenIdConnectMessage message, string nonce)
Convert.ToBase64String(Encoding.UTF8.GetBytes(Options.StateDataFormat.Protect(properties))),
new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure,
Expires = DateTime.UtcNow + Options.ProtocolValidator.NonceLifetime
@@ -667,6 +668,7 @@ protected virtual string RetrieveNonce(OpenIdConnectMessage message)
{
var cookieOptions = new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure
};
diff --git a/src/Microsoft.Owin.Security/Infrastructure/AuthenticationHandler.cs b/src/Microsoft.Owin.Security/Infrastructure/AuthenticationHandler.cs
index db5373c1..8a9bbbf8 100644
--- a/src/Microsoft.Owin.Security/Infrastructure/AuthenticationHandler.cs
+++ b/src/Microsoft.Owin.Security/Infrastructure/AuthenticationHandler.cs
@@ -215,6 +215,7 @@ protected void GenerateCorrelationId(AuthenticationProperties properties)
var cookieOptions = new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure
};
@@ -243,6 +244,7 @@ protected void GenerateCorrelationId(ICookieManager cookieManager, Authenticatio
var cookieOptions = new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure
};
@@ -277,6 +279,7 @@ protected bool ValidateCorrelationId(AuthenticationProperties properties, ILogge
var cookieOptions = new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure
};
@@ -332,6 +335,7 @@ protected bool ValidateCorrelationId(ICookieManager cookieManager, Authenticatio
var cookieOptions = new CookieOptions
{
+ SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = Request.IsSecure
};
diff --git a/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs b/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs
index 8daf4927..4c3f87cb 100644
--- a/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs
+++ b/src/Microsoft.Owin/Infrastructure/ChunkingCookieManager.cs
@@ -136,6 +136,7 @@ public void AppendResponseCookie(IOwinContext context, string key, string value,
bool domainHasValue = !string.IsNullOrEmpty(options.Domain);
bool pathHasValue = !string.IsNullOrEmpty(options.Path);
bool expiresHasValue = options.Expires.HasValue;
+ bool sameSiteHasValue = options.SameSite.HasValue;
string escapedKey = Uri.EscapeDataString(key);
string prefix = escapedKey + "=";
@@ -146,9 +147,11 @@ public void AppendResponseCookie(IOwinContext context, string key, string value,
!pathHasValue ? null : "; path=",
!pathHasValue ? null : options.Path,
!expiresHasValue ? null : "; expires=",
- !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss ", CultureInfo.InvariantCulture) + "GMT",
+ !expiresHasValue ? null : options.Expires.Value.ToString("ddd, dd-MMM-yyyy HH:mm:ss \\G\\M\\T", CultureInfo.InvariantCulture),
!options.Secure ? null : "; secure",
- !options.HttpOnly ? null : "; HttpOnly");
+ !options.HttpOnly ? null : "; HttpOnly",
+ !sameSiteHasValue ? null : "; SameSite=",
+ !sameSiteHasValue ? null : GetStringRepresentationOfSameSite(options.SameSite.Value));
value = value ?? string.Empty;
bool quoted = false;
@@ -277,6 +280,9 @@ public void DeleteCookie(IOwinContext context, string key, CookieOptions options
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ SameSite = options.SameSite,
+ Secure = options.Secure,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
@@ -290,6 +296,9 @@ public void DeleteCookie(IOwinContext context, string key, CookieOptions options
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ SameSite = options.SameSite,
+ Secure = options.Secure,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
@@ -309,5 +318,21 @@ private static string Quote(string value)
{
return '"' + value + '"';
}
+
+ private static string GetStringRepresentationOfSameSite(SameSiteMode siteMode)
+ {
+ switch (siteMode)
+ {
+ case SameSiteMode.None:
+ return "None";
+ case SameSiteMode.Lax:
+ return "Lax";
+ case SameSiteMode.Strict:
+ return "Strict";
+ default:
+ throw new ArgumentOutOfRangeException("siteMode",
+ string.Format(CultureInfo.InvariantCulture, "Unexpected SameSiteMode value: {0}", siteMode));
+ }
+ }
}
}
diff --git a/src/Microsoft.Owin/ResponseCookieCollection.cs b/src/Microsoft.Owin/ResponseCookieCollection.cs
index 0540936f..8c6a0be3 100644
--- a/src/Microsoft.Owin/ResponseCookieCollection.cs
+++ b/src/Microsoft.Owin/ResponseCookieCollection.cs
@@ -138,14 +138,13 @@ public void Delete(string key, CookieOptions options)
{
Path = options.Path,
Domain = options.Domain,
+ HttpOnly = options.HttpOnly,
+ SameSite = options.SameSite,
+ Secure = options.Secure,
Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc),
});
}
- ///
- /// Analogous to ToString() but without boxing so
- /// we can save a bit of memory.
- ///
private static string GetStringRepresentationOfSameSite(SameSiteMode siteMode)
{
switch (siteMode)
diff --git a/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj b/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj
index 62edb256..76b02d1f 100644
--- a/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj
+++ b/tests/Katana.Sandbox.WebServer/Katana.Sandbox.WebServer.csproj
@@ -101,6 +101,7 @@
+
diff --git a/tests/Katana.Sandbox.WebServer/SameSiteCookieManager.cs b/tests/Katana.Sandbox.WebServer/SameSiteCookieManager.cs
new file mode 100644
index 00000000..d5f8869a
--- /dev/null
+++ b/tests/Katana.Sandbox.WebServer/SameSiteCookieManager.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using Microsoft.Owin;
+using Microsoft.Owin.Infrastructure;
+
+namespace Katana.Sandbox.WebServer
+{
+ public class SameSiteCookieManager : ICookieManager
+ {
+ private readonly ICookieManager _innerManager;
+
+ public SameSiteCookieManager()
+ : this(new CookieManager())
+ {
+ }
+
+ public SameSiteCookieManager(ICookieManager innerManager)
+ {
+ _innerManager = innerManager;
+ }
+
+ public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options)
+ {
+ CheckSameSite(context, options);
+ _innerManager.AppendResponseCookie(context, key, value, options);
+ }
+
+ public void DeleteCookie(IOwinContext context, string key, CookieOptions options)
+ {
+ CheckSameSite(context, options);
+ _innerManager.DeleteCookie(context, key, options);
+ }
+
+ public string GetRequestCookie(IOwinContext context, string key)
+ {
+ return _innerManager.GetRequestCookie(context, key);
+ }
+
+ private void CheckSameSite(IOwinContext context, CookieOptions options)
+ {
+ if (DisallowsSameSiteNone(context) && options.SameSite == SameSiteMode.None)
+ {
+ // IOS12 and Mac OS X 10.14 treat SameSite=None as SameSite=Strict. Exclude the option instead.
+ // https://bugs.webkit.org/show_bug.cgi?id=198181
+ options.SameSite = null;
+ }
+ }
+
+ // https://myip.ms/view/comp_browsers/8568/Safari_12.html
+ public static bool DisallowsSameSiteNone(IOwinContext context)
+ {
+ // TODO: Use your User Agent library of choice here.
+ var userAgent = context.Request.Headers["User-Agent"];
+ return 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");
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Katana.Sandbox.WebServer/Startup.cs b/tests/Katana.Sandbox.WebServer/Startup.cs
index bd771020..235ea7ee 100644
--- a/tests/Katana.Sandbox.WebServer/Startup.cs
+++ b/tests/Katana.Sandbox.WebServer/Startup.cs
@@ -5,6 +5,7 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
+using System.Net;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
@@ -32,6 +33,9 @@ public class Startup
public void Configuration(IAppBuilder app)
{
+ // For twitter:
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
+
var logger = app.CreateLogger("Katana.Sandbox.WebServer");
logger.WriteInformation("Application Started");
@@ -59,7 +63,8 @@ public void Configuration(IAppBuilder app)
AuthenticationMode = AuthenticationMode.Active,
CookieName = CookieAuthenticationDefaults.CookiePrefix + "External",
ExpireTimeSpan = TimeSpan.FromMinutes(5),
- CookieManager = new SystemWebChunkingCookieManager()
+ // CookieManager = new SystemWebChunkingCookieManager()
+ CookieManager = new SameSiteCookieManager()
});
// https://developers.facebook.com/apps/
@@ -69,7 +74,7 @@ public void Configuration(IAppBuilder app)
AppSecret = Environment.GetEnvironmentVariable("facebook:appsecret"),
Scope = { "email" },
Fields = { "name", "email" },
- CookieManager = new SystemWebCookieManager()
+ // CookieManager = new SystemWebCookieManager()
});
// https://console.developers.google.com/apis/credentials
@@ -125,15 +130,22 @@ public void Configuration(IAppBuilder app)
{
Wtrealm = "https://tratcheroutlook.onmicrosoft.com/AspNetCoreSample",
MetadataAddress = "https://login.windows.net/cdc690f9-b6b8-4023-813a-bae7143d1f87/FederationMetadata/2007-06/FederationMetadata.xml",
+ Wreply = "https://localhost:44318/",
});
app.UseOpenIdConnectAuthentication(new Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions()
{
+ // https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs
+ ClientId = "server.hybrid",
+ ClientSecret = "secret", // for code flow
+ Authority = "https://demo.identityserver.io/",
+ /*
Authority = Environment.GetEnvironmentVariable("oidc:authority"),
ClientId = Environment.GetEnvironmentVariable("oidc:clientid"),
- ClientSecret = Environment.GetEnvironmentVariable("oidc:clientsecret"),
RedirectUri = "https://localhost:44318/",
- CookieManager = new SystemWebCookieManager(),
+ ClientSecret = Environment.GetEnvironmentVariable("oidc:clientsecret"),*/
+ // CookieManager = new SystemWebCookieManager(),
+ CookieManager = new SameSiteCookieManager(),
//ResponseType = "code",
//ResponseMode = "query",
//SaveTokens = true,