Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding a SessionStore (ITicketStore) to my application cookie makes my Data Protection Provider fail to work

tl;dr

  • Have .NET Core 2.0 application which uses a Data Protection Provider which persists a key file across all of the sites on my domain.
  • Worked fine, however, application cookie became too big.
  • Implemented a SessionStore on the cookie using ITicketStore
  • Cookie size is greatly reduced, however, the key from the DPP no longer persists across my sites.

Is there something I'm supposed to do in my ITicketStore implementation to fix this? I'm assuming so, since this is where the problem arises, however, I could not figure it out.

Some snippets:


Startup.cs --> ConfigureServices()

var keysFolder = $@"c:\temp\_WebAppKeys\{_env.EnvironmentName.ToLower()}";
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(keysFolder));
var dataProtector = protectionProvider.CreateProtector(
            "Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationMiddleware",
            "Cookies",
            "v2");

--snip--

services.AddSingleton<ITicketStore, TicketStore>();

--snip--

services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
    .SetApplicationName("app_auth");

services.ConfigureApplicationCookie(options =>
{
    options.Cookie.Name = ".XAUTH";
    options.Cookie.Domain = ".domain.com";
    options.ExpireTimeSpan = TimeSpan.FromDays(7);
    options.LoginPath = "/Account/Login";
    options.DataProtectionProvider = protectionProvider;
    options.TicketDataFormat = new TicketDataFormat(dataProtector);
    options.CookieManager = new ChunkingCookieManager();
    options.SessionStore = services.BuildServiceProvider().GetService<ITicketStore>();
});

TicketStore.cs

public class TicketStore : ITicketStore
{
    private IMemoryCache _cache;
    private const string KeyPrefix = "AuthSessionStore-";

public TicketStore(IMemoryCache cache)
{
    _cache = cache;
}

public Task RemoveAsync(string key)
{
    _cache.Remove(key);
    return Task.FromResult(0);
}

public Task RenewAsync(string key, AuthenticationTicket ticket)
{
    var options = new MemoryCacheEntryOptions
    {
        Priority = CacheItemPriority.NeverRemove
    };
    var expiresUtc = ticket.Properties.ExpiresUtc;

    if (expiresUtc.HasValue)
    {
        options.SetAbsoluteExpiration(expiresUtc.Value);
    }

    options.SetSlidingExpiration(TimeSpan.FromMinutes(60));

    _cache.Set(key, ticket, options);

    return Task.FromResult(0);
}

public Task<AuthenticationTicket> RetrieveAsync(string key)
{
    AuthenticationTicket ticket;
    _cache.TryGetValue(key, out ticket);
    return Task.FromResult(ticket);
}

public async Task<string> StoreAsync(AuthenticationTicket ticket)
{
    var key = KeyPrefix + Guid.NewGuid();
    await RenewAsync(key, ticket);
    return key;
}
like image 347
Daath Avatar asked Sep 03 '25 09:09

Daath


1 Answers

I also ran into this issue.

The SessionIdClaim value in Microsoft.Owin.Security.Cookies is "Microsoft.Owin.Security.Cookies-SessionId", while the SessionIdClaim value in Microsoft.AspNetCore.Authentication.Cookies is "Microsoft.AspNetCore.Authentication.Cookies-SessionId".

This results in a SessionId Missing error due to this code on the AspNetCore side even when you implemented a distributed session store (using RedisCacheTicketStore for example) as decribed here: https://mikerussellnz.github.io/.NET-Core-Auth-Ticket-Redis/

I was able to re-compile the AspNetKatana project with the new string, and then the SessionID was found on the .NET Core side.

Additionally, it seems the AuthenticationTicket classes are different, so I was able to get this working by implementing a conversion method to convert the Microsoft.Owin.Security.AuthenticationTicket Ticket to the Microsoft.AspNetCore.Authentication.AuthenticationTicket Ticket and then store the ticket using the AspNetCore serializer (Microsoft.AspNetCore.Authentication.TicketSerializer).

public Microsoft.AspNetCore.Authentication.AuthenticationTicket ConvertTicket(Microsoft.Owin.Security.AuthenticationTicket ticket)
{
    Microsoft.AspNetCore.Authentication.AuthenticationProperties netCoreAuthProps = new Microsoft.AspNetCore.Authentication.AuthenticationProperties();
    netCoreAuthProps.IssuedUtc = ticket.Properties.IssuedUtc;
    netCoreAuthProps.ExpiresUtc = ticket.Properties.ExpiresUtc;
    netCoreAuthProps.IsPersistent = ticket.Properties.IsPersistent;
    netCoreAuthProps.AllowRefresh = ticket.Properties.AllowRefresh;
    netCoreAuthProps.RedirectUri = ticket.Properties.RedirectUri;

    ClaimsPrincipal cp = new ClaimsPrincipal(ticket.Identity);

    Microsoft.AspNetCore.Authentication.AuthenticationTicket netCoreTicket = new Microsoft.AspNetCore.Authentication.AuthenticationTicket(cp, netCoreAuthProps, "Cookies");

    return netCoreTicket;
}   
private static Microsoft.AspNetCore.Authentication.TicketSerializer _netCoreSerializer = Microsoft.AspNetCore.Authentication.TicketSerializer.Default;

private static byte[] SerializeToBytesNetCore(Microsoft.AspNetCore.Authentication.AuthenticationTicket source)
{
    return _netCoreSerializer.Serialize(source);
}

With these additional methods, the RenwAsync method can be changed to this:

      public Task RenewAsync(string key, Microsoft.Owin.Security.AuthenticationTicket ticket)
        {
            var options = new DistributedCacheEntryOptions();
            var expiresUtc = ticket.Properties.ExpiresUtc;
            if (expiresUtc.HasValue)
            {
                options.SetAbsoluteExpiration(expiresUtc.Value);
            }

            var netCoreTicket = ConvertTicket(ticket);                      
// convert to .NET Core format     
            byte[] netCoreVal = SerializeToBytesNetCore(netCoreTicket);     
// serialize ticket using .NET Core Serializer
            _cache.Set(key, netCoreVal, options);


            return Task.FromResult(0);
        }

I am not sure if this is the best approach, but it seems to work on my test project, admittedly I am not using this in production, hopefully this helps.

UPDATE #1: Alternate approach to avoid re-compiling

It looks like this might also work by re-creating the cookie with both SessionId claim values on the OWIN side. This will allow you to use the standard library without re-compiling. I tried it this morning but have not had a chance to thoroughly test it, although on my initial test it does load the claims properly on both sides. Basically, if you modify the authentication ticket to have both SessionId claims, it will find the session in both applications. This code snippet gets the cookie, unprotects it, adds the additional claim, and then replaces the cookie inside the OnValidateIdentity event of the CookieAuthenticationProvider.

string cookieName = "myappname";
string KatanaSessionIdClaim = "Microsoft.Owin.Security.Cookies-SessionId";
string NetCoreSessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";

Microsoft.Owin.Security.Interop.ChunkingCookieManager cookieMgr = new ChunkingCookieManager();

OnValidateIdentity = ctx =>
{
    var incomingIdentity = ctx.Identity;
    var cookie = cookieMgr.GetRequestCookie(ctx.OwinContext, cookieName);
    if (cookie != null)
    {
        var ticket = TicketDataFormat.Unprotect(cookie);
        if (ticket != null)
        {
            Claim claim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(KatanaSessionIdClaim));
            Claim netCoreSessionClaim = ticket.Identity.Claims.FirstOrDefault(c => c.Type.Equals(NetCoreSessionIdClaim));
            if (netCoreSessionClaim == null)
            {
                // adjust cookie options as needed.
                CookieOptions opts = new CookieOptions();
                opts.Expires = ticket.Properties.ExpiresUtc == null ? 
    DateTime.Now.AddDays(14) : ticket.Properties.ExpiresUtc.Value.DateTime;
                opts.HttpOnly = true;
                opts.Path = "/";
                opts.Secure = true;

                netCoreSessionClaim = new Claim(NetCoreSessionIdClaim, claim.Value);
                ticket.Identity.AddClaim(netCoreSessionClaim);
                string newCookieValue = TicketDataFormat.Protect(ticket);
                cookieMgr.DeleteCookie(ctx.OwinContext, cookieName, opts);
                cookieMgr.AppendResponseCookie(ctx.OwinContext, cookieName, newCookieValue, opts);
            }
        }
    }
}

If there is a better approach I would be curious to know, or a better place to swap out the cookie.

like image 159
Anthony Valeri Avatar answered Sep 05 '25 00:09

Anthony Valeri