#105 Use DataProtection to encrypt the cookie

This commit is contained in:
Chris R 2016-04-29 13:17:39 -07:00
parent 627dfdc646
commit dabd28a5d9
9 changed files with 127 additions and 13 deletions

View File

@ -15,10 +15,12 @@
"Hosting:Environment": "Development" "Hosting:Environment": "Development"
} }
}, },
"web": { "SessionSample": {
"commandName": "web", "commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000/",
"environmentVariables": { "environmentVariables": {
"Hosting:Environment": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
} }
} }

View File

@ -27,11 +27,11 @@ namespace SessionSample
// o.SchemaName = "dbo"; // o.SchemaName = "dbo";
// o.TableName = "Sessions"; // o.TableName = "Sessions";
//}); //});
#if NET451
// Uncomment the following line to use the Redis implementation of IDistributedCache. // Uncomment the following line to use the Redis implementation of IDistributedCache.
// This will override any previously registered IDistributedCache service. // This will override any previously registered IDistributedCache service.
//services.AddSingleton<IDistributedCache, RedisCache>(); //services.AddSingleton<IDistributedCache, RedisCache>();
#endif
// Adds a default in-memory implementation of IDistributedCache // Adds a default in-memory implementation of IDistributedCache
services.AddMemoryCache(); services.AddMemoryCache();
services.AddDistributedMemoryCache(); services.AddDistributedMemoryCache();

View File

@ -8,7 +8,6 @@
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*",
"Microsoft.AspNetCore.Session": "1.0.0-*", "Microsoft.AspNetCore.Session": "1.0.0-*",
"Microsoft.Extensions.Caching.Memory": "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.Caching.SqlServer": "1.0.0-*",
"Microsoft.Extensions.Logging.Console": "1.0.0-*" "Microsoft.Extensions.Logging.Console": "1.0.0-*"
}, },
@ -18,7 +17,22 @@
] ]
}, },
"frameworks": { "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": { "tools": {
"Microsoft.AspNetCore.Server.IISIntegration.Tools": { "Microsoft.AspNetCore.Server.IISIntegration.Tools": {

View File

@ -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);
}
}
}

View File

@ -12,6 +12,7 @@ namespace Microsoft.Extensions.Logging
private static Action<ILogger, string, string, Exception> _sessionStarted; private static Action<ILogger, string, string, Exception> _sessionStarted;
private static Action<ILogger, string, string, int, Exception> _sessionLoaded; private static Action<ILogger, string, string, int, Exception> _sessionLoaded;
private static Action<ILogger, string, string, int, Exception> _sessionStored; private static Action<ILogger, string, string, int, Exception> _sessionStored;
private static Action<ILogger, Exception> _errorUnprotectingCookie;
static LoggingExtensions() static LoggingExtensions()
{ {
@ -35,6 +36,10 @@ namespace Microsoft.Extensions.Logging
eventId: 5, eventId: 5,
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
formatString: "Session stored; Key:{sessionKey}, Id:{sessionId}, Count:{count}"); 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) public static void ErrorClosingTheSession(this ILogger logger, Exception exception)
@ -61,5 +66,10 @@ namespace Microsoft.Extensions.Logging
{ {
_sessionStored(logger, sessionKey, sessionId, count, null); _sessionStored(logger, sessionKey, sessionId, count, null);
} }
public static void ErrorUnprotectingSessionCookie(this ILogger logger, Exception exception)
{
_errorUnprotectingCookie(logger, exception);
}
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -24,17 +25,20 @@ namespace Microsoft.AspNetCore.Session
private readonly SessionOptions _options; private readonly SessionOptions _options;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly ISessionStore _sessionStore; private readonly ISessionStore _sessionStore;
private readonly IDataProtector _dataProtector;
/// <summary> /// <summary>
/// Creates a new <see cref="SessionMiddleware"/>. /// Creates a new <see cref="SessionMiddleware"/>.
/// </summary> /// </summary>
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param> /// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> representing the factory that used to create logger instances.</param> /// <param name="loggerFactory">The <see cref="ILoggerFactory"/> representing the factory that used to create logger instances.</param>
/// <param name="dataProtectionProvider">The <see cref="IDataProtectionProvider"/> used to protect and verify the cookie.</param>
/// <param name="sessionStore">The <see cref="ISessionStore"/> representing the session store.</param> /// <param name="sessionStore">The <see cref="ISessionStore"/> representing the session store.</param>
/// <param name="options">The session configuration options.</param> /// <param name="options">The session configuration options.</param>
public SessionMiddleware( public SessionMiddleware(
RequestDelegate next, RequestDelegate next,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
IDataProtectionProvider dataProtectionProvider,
ISessionStore sessionStore, ISessionStore sessionStore,
IOptions<SessionOptions> options) IOptions<SessionOptions> options)
{ {
@ -48,6 +52,11 @@ namespace Microsoft.AspNetCore.Session
throw new ArgumentNullException(nameof(loggerFactory)); throw new ArgumentNullException(nameof(loggerFactory));
} }
if (dataProtectionProvider == null)
{
throw new ArgumentNullException(nameof(dataProtectionProvider));
}
if (sessionStore == null) if (sessionStore == null)
{ {
throw new ArgumentNullException(nameof(sessionStore)); throw new ArgumentNullException(nameof(sessionStore));
@ -60,6 +69,7 @@ namespace Microsoft.AspNetCore.Session
_next = next; _next = next;
_logger = loggerFactory.CreateLogger<SessionMiddleware>(); _logger = loggerFactory.CreateLogger<SessionMiddleware>();
_dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware));
_options = options.Value; _options = options.Value;
_sessionStore = sessionStore; _sessionStore = sessionStore;
} }
@ -73,14 +83,16 @@ namespace Microsoft.AspNetCore.Session
{ {
var isNewSessionKey = false; var isNewSessionKey = false;
Func<bool> tryEstablishSession = ReturnTrue; Func<bool> 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) if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
{ {
// No valid cookie, new session. // No valid cookie, new session.
var guidBytes = new byte[16]; var guidBytes = new byte[16];
CryptoRandom.GetBytes(guidBytes); CryptoRandom.GetBytes(guidBytes);
sessionKey = new Guid(guidBytes).ToString(); 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; tryEstablishSession = establisher.TryEstablishSession;
isNewSessionKey = true; isNewSessionKey = true;
} }
@ -114,14 +126,14 @@ namespace Microsoft.AspNetCore.Session
private class SessionEstablisher private class SessionEstablisher
{ {
private readonly HttpContext _context; private readonly HttpContext _context;
private readonly string _sessionKey; private readonly string _cookieValue;
private readonly SessionOptions _options; private readonly SessionOptions _options;
private bool _shouldEstablishSession; private bool _shouldEstablishSession;
public SessionEstablisher(HttpContext context, string sessionKey, SessionOptions options) public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
{ {
_context = context; _context = context;
_sessionKey = sessionKey; _cookieValue = cookieValue;
_options = options; _options = options;
context.Response.OnStarting(OnStartingCallback, state: this); context.Response.OnStarting(OnStartingCallback, state: this);
} }
@ -145,7 +157,7 @@ namespace Microsoft.AspNetCore.Session
Path = _options.CookiePath ?? SessionDefaults.CookiePath, 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["Cache-Control"] = "no-cache";
_context.Response.Headers["Pragma"] = "no-cache"; _context.Response.Headers["Pragma"] = "no-cache";

View File

@ -25,6 +25,7 @@ namespace Microsoft.Extensions.DependencyInjection
} }
services.AddTransient<ISessionStore, DistributedSessionStore>(); services.AddTransient<ISessionStore, DistributedSessionStore>();
services.AddDataProtection();
return services; return services;
} }

View File

@ -13,6 +13,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"Microsoft.AspNetCore.DataProtection": "1.0.0-*",
"Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*", "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
"Microsoft.Extensions.Caching.Abstractions": "1.0.0-*", "Microsoft.Extensions.Caching.Abstractions": "1.0.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.0.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.0.0-*",

View File

@ -13,5 +13,8 @@
<PropertyGroup> <PropertyGroup>
<SchemaVersion>2.0</SchemaVersion> <SchemaVersion>2.0</SchemaVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" /> <Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
</Project> </Project>