diff --git a/samples/SessionSample/Properties/launchSettings.json b/samples/SessionSample/Properties/launchSettings.json index bd71d7713a..9137a2c6f4 100644 --- a/samples/SessionSample/Properties/launchSettings.json +++ b/samples/SessionSample/Properties/launchSettings.json @@ -15,10 +15,12 @@ "Hosting:Environment": "Development" } }, - "web": { - "commandName": "web", + "SessionSample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/", "environmentVariables": { - "Hosting:Environment": "Development" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/samples/SessionSample/Startup.cs b/samples/SessionSample/Startup.cs index b87fa956c7..8946d5e2df 100644 --- a/samples/SessionSample/Startup.cs +++ b/samples/SessionSample/Startup.cs @@ -27,11 +27,11 @@ namespace SessionSample // o.SchemaName = "dbo"; // o.TableName = "Sessions"; //}); - +#if NET451 // Uncomment the following line to use the Redis implementation of IDistributedCache. // This will override any previously registered IDistributedCache service. //services.AddSingleton(); - +#endif // Adds a default in-memory implementation of IDistributedCache services.AddMemoryCache(); services.AddDistributedMemoryCache(); diff --git a/samples/SessionSample/project.json b/samples/SessionSample/project.json index 349065e475..3b12791852 100644 --- a/samples/SessionSample/project.json +++ b/samples/SessionSample/project.json @@ -8,7 +8,6 @@ "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", "Microsoft.AspNetCore.Session": "1.0.0-*", "Microsoft.Extensions.Caching.Memory": "1.0.0-*", - "Microsoft.Extensions.Caching.Redis": "1.0.0-*", "Microsoft.Extensions.Caching.SqlServer": "1.0.0-*", "Microsoft.Extensions.Logging.Console": "1.0.0-*" }, @@ -18,7 +17,22 @@ ] }, "frameworks": { - "net451": {} + "net451": { + "dependencies": { + "Microsoft.Extensions.Caching.Redis": "1.0.0-*" + } + }, + "netcoreapp1.0": { + "imports": [ + "dnxcore50" + ], + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + } }, "tools": { "Microsoft.AspNetCore.Server.IISIntegration.Tools": { diff --git a/src/Microsoft.AspNetCore.Session/CookieProtection.cs b/src/Microsoft.AspNetCore.Session/CookieProtection.cs new file mode 100644 index 0000000000..64a3a3fbbf --- /dev/null +++ b/src/Microsoft.AspNetCore.Session/CookieProtection.cs @@ -0,0 +1,71 @@ +// 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; +using System.Text; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Session +{ + internal static class CookieProtection + { + internal static string Protect(IDataProtector protector, string data) + { + if (protector == null) + { + throw new ArgumentNullException(nameof(protector)); + } + if (string.IsNullOrEmpty(data)) + { + return data; + } + + var userData = Encoding.UTF8.GetBytes(data); + + var protectedData = protector.Protect(userData); + return Convert.ToBase64String(protectedData).TrimEnd('='); + } + + internal static string Unprotect(IDataProtector protector, string protectedText, ILogger logger) + { + try + { + if (string.IsNullOrEmpty(protectedText)) + { + return string.Empty; + } + + var protectedData = Convert.FromBase64String(Pad(protectedText)); + if (protectedData == null) + { + return string.Empty; + } + + var userData = protector.Unprotect(protectedData); + if (userData == null) + { + return string.Empty; + } + + return Encoding.UTF8.GetString(userData); + } + catch (Exception ex) + { + // Log the exception, but do not leak other information + logger.ErrorUnprotectingSessionCookie(ex); + return string.Empty; + } + } + + private static string Pad(string text) + { + var padding = 3 - ((text.Length + 3) % 4); + if (padding == 0) + { + return text; + } + return text + new string('=', padding); + } + } +} diff --git a/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs b/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs index 1dcc93e709..f2bb1be87d 100644 --- a/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs +++ b/src/Microsoft.AspNetCore.Session/LoggingExtensions.cs @@ -12,6 +12,7 @@ namespace Microsoft.Extensions.Logging private static Action _sessionStarted; private static Action _sessionLoaded; private static Action _sessionStored; + private static Action _errorUnprotectingCookie; static LoggingExtensions() { @@ -35,6 +36,10 @@ namespace Microsoft.Extensions.Logging eventId: 5, logLevel: LogLevel.Debug, formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}"); + _errorUnprotectingCookie = LoggerMessage.Define( + eventId: 6, + logLevel: LogLevel.Warning, + formatString: "Error unprotecting the session cookie."); } public static void ErrorClosingTheSession(this ILogger logger, Exception exception) @@ -61,5 +66,10 @@ namespace Microsoft.Extensions.Logging { _sessionStored(logger, sessionKey, sessionId, count, null); } + + public static void ErrorUnprotectingSessionCookie(this ILogger logger, Exception exception) + { + _errorUnprotectingCookie(logger, exception); + } } } diff --git a/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs b/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs index 23ccd289ce..8f6ff452f6 100644 --- a/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs +++ b/src/Microsoft.AspNetCore.Session/SessionMiddleware.cs @@ -5,6 +5,7 @@ using System; using System.Security.Cryptography; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; @@ -24,17 +25,20 @@ namespace Microsoft.AspNetCore.Session private readonly SessionOptions _options; private readonly ILogger _logger; private readonly ISessionStore _sessionStore; + private readonly IDataProtector _dataProtector; /// /// Creates a new . /// /// The representing the next middleware in the pipeline. /// The representing the factory that used to create logger instances. + /// The used to protect and verify the cookie. /// The representing the session store. /// The session configuration options. public SessionMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, + IDataProtectionProvider dataProtectionProvider, ISessionStore sessionStore, IOptions options) { @@ -48,6 +52,11 @@ namespace Microsoft.AspNetCore.Session throw new ArgumentNullException(nameof(loggerFactory)); } + if (dataProtectionProvider == null) + { + throw new ArgumentNullException(nameof(dataProtectionProvider)); + } + if (sessionStore == null) { throw new ArgumentNullException(nameof(sessionStore)); @@ -60,6 +69,7 @@ namespace Microsoft.AspNetCore.Session _next = next; _logger = loggerFactory.CreateLogger(); + _dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware)); _options = options.Value; _sessionStore = sessionStore; } @@ -73,14 +83,16 @@ namespace Microsoft.AspNetCore.Session { var isNewSessionKey = false; Func tryEstablishSession = ReturnTrue; - string sessionKey = context.Request.Cookies[_options.CookieName]; + var cookieValue = context.Request.Cookies[_options.CookieName]; + var sessionKey = CookieProtection.Unprotect(_dataProtector, cookieValue, _logger); if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength) { // No valid cookie, new session. var guidBytes = new byte[16]; CryptoRandom.GetBytes(guidBytes); sessionKey = new Guid(guidBytes).ToString(); - var establisher = new SessionEstablisher(context, sessionKey, _options); + cookieValue = CookieProtection.Protect(_dataProtector, sessionKey); + var establisher = new SessionEstablisher(context, cookieValue, _options); tryEstablishSession = establisher.TryEstablishSession; isNewSessionKey = true; } @@ -114,14 +126,14 @@ namespace Microsoft.AspNetCore.Session private class SessionEstablisher { private readonly HttpContext _context; - private readonly string _sessionKey; + private readonly string _cookieValue; private readonly SessionOptions _options; private bool _shouldEstablishSession; - public SessionEstablisher(HttpContext context, string sessionKey, SessionOptions options) + public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options) { _context = context; - _sessionKey = sessionKey; + _cookieValue = cookieValue; _options = options; context.Response.OnStarting(OnStartingCallback, state: this); } @@ -145,7 +157,7 @@ namespace Microsoft.AspNetCore.Session Path = _options.CookiePath ?? SessionDefaults.CookiePath, }; - _context.Response.Cookies.Append(_options.CookieName, _sessionKey, cookieOptions); + _context.Response.Cookies.Append(_options.CookieName, _cookieValue, cookieOptions); _context.Response.Headers["Cache-Control"] = "no-cache"; _context.Response.Headers["Pragma"] = "no-cache"; diff --git a/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs index c078a12fac..48424e48cd 100644 --- a/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Session/SessionServiceCollectionExtensions.cs @@ -25,6 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddTransient(); + services.AddDataProtection(); return services; } diff --git a/src/Microsoft.AspNetCore.Session/project.json b/src/Microsoft.AspNetCore.Session/project.json index 22720aad9e..7eb81db2b5 100644 --- a/src/Microsoft.AspNetCore.Session/project.json +++ b/src/Microsoft.AspNetCore.Session/project.json @@ -13,6 +13,7 @@ ] }, "dependencies": { + "Microsoft.AspNetCore.DataProtection": "1.0.0-*", "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", "Microsoft.Extensions.Caching.Abstractions": "1.0.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", diff --git a/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.xproj b/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.xproj index 2fc2960ef9..c030463c79 100644 --- a/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.xproj +++ b/test/Microsoft.AspNetCore.Session.Tests/Microsoft.AspNetCore.Session.Tests.xproj @@ -13,5 +13,8 @@ 2.0 + + + \ No newline at end of file