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,