diff --git a/src/Microsoft.AspNet.Session/CookieSecureOption.cs b/src/Microsoft.AspNet.Session/CookieSecureOption.cs new file mode 100644 index 0000000..0395024 --- /dev/null +++ b/src/Microsoft.AspNet.Session/CookieSecureOption.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Session +{ + /// + /// Determines how the identity cookie's security property is set. + /// + public enum CookieSecureOption + { + /// + /// If the URI that provides the cookie is HTTPS, then the cookie will only be returned to the server on + /// subsequent HTTPS requests. Otherwise if the URI that provides the cookie is HTTP, then the cookie will + /// be returned to the server on all HTTP and HTTPS requests. This is the default value because it ensures + /// HTTPS for all authenticated requests on deployed servers, and also supports HTTP for localhost development + /// and for servers that do not have HTTPS support. + /// + SameAsRequest, + + /// + /// CookieOptions.Secure is never marked true. Use this value when your login page is HTTPS, but other pages + /// on the site which are HTTP also require authentication information. This setting is not recommended because + /// the authentication information provided with an HTTP request may be observed and used by other computers + /// on your local network or wireless connection. + /// + Never, + + /// + /// CookieOptions.Secure is always marked true. Use this value when your login page and all subsequent pages + /// requiring the authenticated identity are HTTPS. Local development will also need to be done with HTTPS urls. + /// + Always, + } +} diff --git a/src/Microsoft.AspNet.Session/SessionDefaults.cs b/src/Microsoft.AspNet.Session/SessionDefaults.cs index 7fb15b3..84425ad 100644 --- a/src/Microsoft.AspNet.Session/SessionDefaults.cs +++ b/src/Microsoft.AspNet.Session/SessionDefaults.cs @@ -9,5 +9,7 @@ public static class SessionDefaults { public static string CookieName = ".AspNet.Session"; public static string CookiePath = "/"; + public static CookieSecureOption CookieSecure = CookieSecureOption.SameAsRequest; + public static bool CookieHTTPOnly = true; } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Session/SessionMiddleware.cs b/src/Microsoft.AspNet.Session/SessionMiddleware.cs index b5db2ab..e8d918c 100644 --- a/src/Microsoft.AspNet.Session/SessionMiddleware.cs +++ b/src/Microsoft.AspNet.Session/SessionMiddleware.cs @@ -130,6 +130,15 @@ private void SetCookie() Path = _options.CookiePath ?? SessionDefaults.CookiePath, }; + if (_options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = _context.Request.IsHttps; + } + else + { + cookieOptions.Secure = _options.CookieSecure == CookieSecureOption.Always; + } + _context.Response.Cookies.Append(_options.CookieName, _sessionKey, cookieOptions); _context.Response.Headers.Set( diff --git a/src/Microsoft.AspNet.Session/SessionOptions.cs b/src/Microsoft.AspNet.Session/SessionOptions.cs index 345df18..a6965c4 100644 --- a/src/Microsoft.AspNet.Session/SessionOptions.cs +++ b/src/Microsoft.AspNet.Session/SessionOptions.cs @@ -27,7 +27,7 @@ public class SessionOptions /// default is true, which means the cookie will only be passed to HTTP requests and is not made available /// to script on the page. /// - public bool CookieHttpOnly { get; set; } = true; + public bool CookieHttpOnly { get; set; } = SessionDefaults.CookieHTTPOnly; /// /// The IdleTimeout indicates how long the session can be idle before its contents are abandoned. Each session access @@ -35,6 +35,13 @@ public class SessionOptions /// public TimeSpan IdleTimeout { get; set; } = TimeSpan.FromMinutes(20); + /// + /// 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 + /// and portions of your site are HTTP you may need to change this value. + /// + public CookieSecureOption CookieSecure { get; set; } = SessionDefaults.CookieSecure; + /// /// Gets or sets the session storage manager. This overrides any session store passed into the middleware constructor. /// diff --git a/test/Microsoft.AspNet.Session.Tests/SessionTests.cs b/test/Microsoft.AspNet.Session.Tests/SessionTests.cs index be29618..8d6ec20 100644 --- a/test/Microsoft.AspNet.Session.Tests/SessionTests.cs +++ b/test/Microsoft.AspNet.Session.Tests/SessionTests.cs @@ -268,5 +268,140 @@ public async Task ExpiredSession_LogsWarning() Assert.Equal(LogLevel.Warning, sink.Writes[1].LogLevel); } } + + [Fact] + public async Task SettingSecureCookieOptionToNeverWillEnsureSecureFlagNotSet() + { + using (var server = TestServer.Create(app => + { + app.UseInMemorySession(null, cookieoption => cookieoption.CookieSecure = CookieSecureOption.Never); + app.Run(context => + { + Assert.Null(context.Session.GetString("Key")); + context.Session.SetString("Key", "Value"); + Assert.Equal("Value", context.Session.GetString("Key")); + return Task.FromResult(0); + }); + }, + services => services.AddOptions())) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Set-Cookie", out values)); + Assert.Equal(1, values.Count()); + Assert.True(!string.IsNullOrWhiteSpace(values.First())); + Assert.DoesNotContain("secure", values.First()); + } + } + + [Fact] + public async Task SettingSecureCookieOptionToAlwaysWillEnsureSecureFlagSet() + { + using (var server = TestServer.Create(app => + { + app.UseInMemorySession(null, cookieoption => cookieoption.CookieSecure = CookieSecureOption.Always); + app.Run(context => + { + Assert.Null(context.Session.GetString("Key")); + context.Session.SetString("Key", "Value"); + Assert.Equal("Value", context.Session.GetString("Key")); + return Task.FromResult(0); + }); + }, + services => services.AddOptions())) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Set-Cookie", out values)); + Assert.Equal(1, values.Count()); + Assert.True(!string.IsNullOrWhiteSpace(values.First())); + Assert.Contains("secure", values.First()); + } + } + + [Fact] + public async Task SettingSecureCookieOptionToSameAsRequestWillEnsureSecureFlagNotSetWhileNotUsingSSL() + { + using (var server = TestServer.Create(app => + { + app.UseInMemorySession(null, cookieoption => cookieoption.CookieSecure = CookieSecureOption.SameAsRequest); + app.Run(context => + { + Assert.Null(context.Session.GetString("Key")); + context.Session.SetString("Key", "Value"); + Assert.Equal("Value", context.Session.GetString("Key")); + return Task.FromResult(0); + }); + }, + services => services.AddOptions())) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Set-Cookie", out values)); + Assert.Equal(1, values.Count()); + Assert.True(!string.IsNullOrWhiteSpace(values.First())); + Assert.DoesNotContain("secure", values.First()); + } + } + + [Fact] + public async Task SettingHTTPOnlyCookieOptionToTrueWillEnsureHTTPOnlyFlagSet() + { + using (var server = TestServer.Create(app => + { + app.UseInMemorySession(null, cookieoption => cookieoption.CookieHttpOnly = true); + app.Run(context => + { + Assert.Null(context.Session.GetString("Key")); + context.Session.SetString("Key", "Value"); + Assert.Equal("Value", context.Session.GetString("Key")); + return Task.FromResult(0); + }); + }, + services => services.AddOptions())) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Set-Cookie", out values)); + Assert.Equal(1, values.Count()); + Assert.True(!string.IsNullOrWhiteSpace(values.First())); + Assert.Contains("httponly", values.First()); + } + } + + [Fact] + public async Task SettingHTTPOnlyCookieOptionToFalseWillEnsureHTTPOnlyFlagNotSet() + { + using (var server = TestServer.Create(app => + { + app.UseInMemorySession(null, cookieoption => cookieoption.CookieHttpOnly = false); + app.Run(context => + { + Assert.Null(context.Session.GetString("Key")); + context.Session.SetString("Key", "Value"); + Assert.Equal("Value", context.Session.GetString("Key")); + return Task.FromResult(0); + }); + }, + services => services.AddOptions())) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + IEnumerable values; + Assert.True(response.Headers.TryGetValues("Set-Cookie", out values)); + Assert.Equal(1, values.Count()); + Assert.True(!string.IsNullOrWhiteSpace(values.First())); + Assert.DoesNotContain("httponly", values.First()); + } + } } } \ No newline at end of file