diff --git a/Security.sln b/Security.sln
index c9951b0ddf..2cc23038e0 100644
--- a/Security.sln
+++ b/Security.sln
@@ -34,6 +34,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.M
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Security.OAuth", "src\Microsoft.AspNet.Security.OAuth\Microsoft.AspNet.Security.OAuth.kproj", "{4A636011-68EE-4CE5-836D-EA8E13CF71E4}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CookieSessionSample", "samples\CookieSessionSample\CookieSessionSample.kproj", "{19711880-46DA-4A26-9E0F-9B2E41D27651}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -144,6 +146,16 @@ Global
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{4A636011-68EE-4CE5-836D-EA8E13CF71E4}.Release|x86.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Any CPU.Build.0 = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {19711880-46DA-4A26-9E0F-9B2E41D27651}.Release|x86.ActiveCfg = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -159,5 +171,6 @@ Global
{C96B77EA-4078-4C31-BDB2-878F11C5E061} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{1FCF26C2-A3C7-4308-B698-4AFC3560BC0C} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
{4A636011-68EE-4CE5-836D-EA8E13CF71E4} = {4D2B6A51-2F9F-44F5-8131-EA5CAC053652}
+ {19711880-46DA-4A26-9E0F-9B2E41D27651} = {F8C0AA27-F3FB-4286-8E4C-47EF86B539FF}
EndGlobalSection
EndGlobal
diff --git a/samples/CookieSample/project.json b/samples/CookieSample/project.json
index c8eabcb68c..405e3ba0d0 100644
--- a/samples/CookieSample/project.json
+++ b/samples/CookieSample/project.json
@@ -6,8 +6,8 @@
"Microsoft.AspNet.HttpFeature": "1.0.0-*",
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
"Microsoft.AspNet.RequestContainer": "1.0.0-*",
- "Microsoft.AspNet.Security": "",
- "Microsoft.AspNet.Security.Cookies": "",
+ "Microsoft.AspNet.Security": "1.0.0-*",
+ "Microsoft.AspNet.Security.Cookies": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
"Microsoft.Framework.DependencyInjection": "1.0.0-*"
},
diff --git a/samples/CookieSessionSample/CookieSessionSample.kproj b/samples/CookieSessionSample/CookieSessionSample.kproj
new file mode 100644
index 0000000000..951ff1cc56
--- /dev/null
+++ b/samples/CookieSessionSample/CookieSessionSample.kproj
@@ -0,0 +1,32 @@
+
+
+
+ 12.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+ Debug
+ AnyCPU
+
+
+
+
+ 19711880-46da-4a26-9e0f-9b2e41d27651
+ Library
+ CookieSessionSample
+
+
+
+ ConsoleDebugger
+
+
+ WebDebugger
+
+
+
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/samples/CookieSessionSample/MemoryCacheSessionStore.cs b/samples/CookieSessionSample/MemoryCacheSessionStore.cs
new file mode 100644
index 0000000000..07adddd048
--- /dev/null
+++ b/samples/CookieSessionSample/MemoryCacheSessionStore.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNet.MemoryCache;
+using Microsoft.AspNet.Security;
+using Microsoft.AspNet.Security.Cookies.Infrastructure;
+
+namespace CookieSessionSample
+{
+ public class MemoryCacheSessionStore : IAuthenticationSessionStore
+ {
+ private const string KeyPrefix = "AuthSessionStore-";
+ private IMemoryCache _cache;
+
+ public MemoryCacheSessionStore()
+ {
+ _cache = new MemoryCache();
+ }
+
+ public async Task StoreAsync(AuthenticationTicket ticket)
+ {
+ var guid = Guid.NewGuid();
+ var key = KeyPrefix + guid.ToString();
+ await RenewAsync(key, ticket);
+ return key;
+ }
+
+ public Task RenewAsync(string key, AuthenticationTicket ticket)
+ {
+ _cache.Set(key, ticket, context =>
+ {
+ var expiresUtc = ticket.Properties.ExpiresUtc;
+ if (expiresUtc.HasValue)
+ {
+ context.SetAbsoluteExpiration(expiresUtc.Value);
+ }
+ context.SetSlidingExpiraiton(TimeSpan.FromHours(1)); // TODO: configurable.
+
+ return (AuthenticationTicket)context.State;
+ });
+ return Task.FromResult(0);
+ }
+
+ public Task RetrieveAsync(string key)
+ {
+ AuthenticationTicket ticket;
+ _cache.TryGetValue(key, out ticket);
+ return Task.FromResult(ticket);
+ }
+
+ public Task RemoveAsync(string key)
+ {
+ _cache.Remove(key);
+ return Task.FromResult(0);
+ }
+ }
+}
diff --git a/samples/CookieSessionSample/Startup.cs b/samples/CookieSessionSample/Startup.cs
new file mode 100644
index 0000000000..c5359a05f3
--- /dev/null
+++ b/samples/CookieSessionSample/Startup.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Security.Cookies;
+
+namespace CookieSessionSample
+{
+ public class Startup
+ {
+ public void Configure(IBuilder app)
+ {
+ app.UseCookieAuthentication(new CookieAuthenticationOptions()
+ {
+ SessionStore = new MemoryCacheSessionStore(),
+ });
+
+ app.Run(async context =>
+ {
+ if (context.User == null || !context.User.Identity.IsAuthenticated)
+ {
+ // Make a large identity
+ var claims = new List(1001);
+ claims.Add(new Claim("name", "bob"));
+ for (int i = 0; i < 1000; i++)
+ {
+ claims.Add(new Claim(ClaimTypes.Role, "SomeRandomGroup" + i, ClaimValueTypes.String, "IssuedByBob", "OriginalIssuerJoe"));
+ }
+ context.Response.SignIn(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationType));
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello First timer");
+ return;
+ }
+
+ context.Response.ContentType = "text/plain";
+ await context.Response.WriteAsync("Hello old timer");
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/samples/CookieSessionSample/project.json b/samples/CookieSessionSample/project.json
new file mode 100644
index 0000000000..7299177120
--- /dev/null
+++ b/samples/CookieSessionSample/project.json
@@ -0,0 +1,37 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.FeatureModel": "1.0.0-*",
+ "Microsoft.AspNet.Hosting": "1.0.0-*",
+ "Microsoft.AspNet.Http": "1.0.0-*",
+ "Microsoft.AspNet.HttpFeature": "1.0.0-*",
+ "Microsoft.AspNet.MemoryCache": "1.0.0-*",
+ "Microsoft.AspNet.PipelineCore": "1.0.0-*",
+ "Microsoft.AspNet.RequestContainer": "1.0.0-*",
+ "Microsoft.AspNet.Security": "1.0.0-*",
+ "Microsoft.AspNet.Security.Cookies": "1.0.0-*",
+ "Microsoft.AspNet.Server.WebListener": "1.0.0-*",
+ "Microsoft.Framework.DependencyInjection": "1.0.0-*"
+ },
+ "commands": { "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:12345" },
+ "frameworks": {
+ "aspnet50": {
+ },
+ "aspnetcore50": {
+ "dependencies": {
+ "System.Collections": "4.0.10.0",
+ "System.Console": "4.0.0.0",
+ "System.Diagnostics.Debug": "4.0.10.0",
+ "System.Diagnostics.Tools": "4.0.0.0",
+ "System.Globalization": "4.0.10.0",
+ "System.IO": "4.0.10.0",
+ "System.Linq": "4.0.0.0",
+ "System.Reflection": "4.0.10.0",
+ "System.Resources.ResourceManager": "4.0.0.0",
+ "System.Runtime": "4.0.20.0",
+ "System.Runtime.Extensions": "4.0.10.0",
+ "System.Runtime.InteropServices": "4.0.20.0",
+ "System.Threading.Tasks": "4.0.10.0"
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs
index 23f66b6c5b..317c27d21a 100644
--- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs
+++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationHandler.cs
@@ -3,6 +3,8 @@
using System;
+using System.Linq;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Security;
@@ -18,12 +20,14 @@ namespace Microsoft.AspNet.Security.Cookies
private const string HeaderNameExpires = "Expires";
private const string HeaderValueNoCache = "no-cache";
private const string HeaderValueMinusOne = "-1";
+ private const string SessionIdClaim = "Microsoft.AspNet.Security.Cookies-SessionId";
private readonly ILogger _logger;
private bool _shouldRenew;
private DateTimeOffset _renewIssuedUtc;
private DateTimeOffset _renewExpiresUtc;
+ private string _sessionKey;
public CookieAuthenticationHandler([NotNull] ILogger logger)
{
@@ -51,12 +55,33 @@ namespace Microsoft.AspNet.Security.Cookies
return null;
}
+ if (Options.SessionStore != null)
+ {
+ Claim claim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
+ if (claim == null)
+ {
+ _logger.WriteWarning(@"SessoinId missing");
+ return null;
+ }
+ _sessionKey = claim.Value;
+ ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
+ if (ticket == null)
+ {
+ _logger.WriteWarning(@"Identity missing in session store");
+ return null;
+ }
+ }
+
DateTimeOffset currentUtc = Options.SystemClock.UtcNow;
DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc;
DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
+ if (Options.SessionStore != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
return null;
}
@@ -95,6 +120,7 @@ namespace Microsoft.AspNet.Security.Cookies
if (shouldSignin || shouldSignout || _shouldRenew)
{
+ AuthenticationTicket model = await AuthenticateAsync();
var cookieOptions = new CookieOptions
{
Domain = Options.CookieDomain,
@@ -144,7 +170,19 @@ namespace Microsoft.AspNet.Security.Cookies
context.CookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime;
}
- var model = new AuthenticationTicket(context.Identity, context.Properties);
+ model = new AuthenticationTicket(context.Identity, context.Properties);
+ if (Options.SessionStore != null)
+ {
+ if (_sessionKey != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
+ _sessionKey = await Options.SessionStore.StoreAsync(model);
+ ClaimsIdentity identity = new ClaimsIdentity(
+ new[] { new Claim(SessionIdClaim, _sessionKey) },
+ Options.AuthenticationType);
+ model = new AuthenticationTicket(identity, null);
+ }
string cookieValue = Options.TicketDataFormat.Protect(model);
Options.CookieManager.AppendResponseCookie(
@@ -155,6 +193,11 @@ namespace Microsoft.AspNet.Security.Cookies
}
else if (shouldSignout)
{
+ if (Options.SessionStore != null && _sessionKey != null)
+ {
+ await Options.SessionStore.RemoveAsync(_sessionKey);
+ }
+
var context = new CookieResponseSignOutContext(
Context,
Options,
@@ -169,11 +212,18 @@ namespace Microsoft.AspNet.Security.Cookies
}
else if (_shouldRenew)
{
- AuthenticationTicket model = await AuthenticateAsync();
-
model.Properties.IssuedUtc = _renewIssuedUtc;
model.Properties.ExpiresUtc = _renewExpiresUtc;
+ if (Options.SessionStore != null && _sessionKey != null)
+ {
+ await Options.SessionStore.RenewAsync(_sessionKey, model);
+ ClaimsIdentity identity = new ClaimsIdentity(
+ new[] { new Claim(SessionIdClaim, _sessionKey) },
+ Options.AuthenticationType);
+ model = new AuthenticationTicket(identity, null);
+ }
+
string cookieValue = Options.TicketDataFormat.Protect(model);
if (model.Properties.IsPersistent)
diff --git a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs
index 0b6ef879e9..c6373a0ec9 100644
--- a/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs
+++ b/src/Microsoft.AspNet.Security.Cookies/CookieAuthenticationOptions.cs
@@ -143,5 +143,11 @@ namespace Microsoft.AspNet.Security.Cookies
/// ChunkingCookieManager will be used by default.
///
public ICookieManager CookieManager { get; set; }
+
+ ///
+ /// An optional container in which to store the identity across requests. When used, only a session identifier is sent
+ /// to the client. This can be used to mitigate potential problems with very large identities.
+ ///
+ public IAuthenticationSessionStore SessionStore { get; set; }
}
}
diff --git a/src/Microsoft.AspNet.Security.Cookies/Infrastructure/IAuthenticationSessionStore.cs b/src/Microsoft.AspNet.Security.Cookies/Infrastructure/IAuthenticationSessionStore.cs
new file mode 100644
index 0000000000..9f449b098b
--- /dev/null
+++ b/src/Microsoft.AspNet.Security.Cookies/Infrastructure/IAuthenticationSessionStore.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNet.Security.Cookies.Infrastructure
+{
+ ///
+ /// This provides an abstract storage mechanic to preserve identity information on the server
+ /// while only sending a simple identifier key to the client. This is most commonly used to mitigate
+ /// issues with serializing large identities into cookies.
+ ///
+ public interface IAuthenticationSessionStore
+ {
+ ///
+ /// Store the identity ticket and return the associated key.
+ ///
+ /// The identity information to store.
+ /// The key that can be used to retrieve the identity later.
+ Task StoreAsync(AuthenticationTicket ticket);
+
+ ///
+ /// Tells the store that the given identity should be updated.
+ ///
+ ///
+ ///
+ ///
+ Task RenewAsync(string key, AuthenticationTicket ticket);
+
+ ///
+ /// Retrieves an identity from the store for the given key.
+ ///
+ /// The key associated with the identity.
+ /// The identity associated with the given key, or if not found.
+ Task RetrieveAsync(string key);
+
+ ///
+ /// Remove the identity associated with the given key.
+ ///
+ /// The key associated with the identity.
+ ///
+ Task RemoveAsync(string key);
+ }
+}