6

I have a .NET Core 3 project (recently upgraded from 2.2) that uses a Redis distributed cache and cookie authentication.

It currently looks something like this:

public void ConfigureServices(IServiceCollection services)
{
    // Set up Redis distributed cache
    services.AddStackExchangeRedisCache(...);

    ...

    services.ConfigureApplicationCookie(options =>
    {
        ...
        // Get a service provider to get the distributed cache set up above
        var cache = services.BuildServiceProvider().GetService<IDistributedCache>();

         options.SessionStore = new MyCustomStore(cache, ...);
    }):
}

The problem is that BuildServiceProvider() causes a build error:

Startup.cs(...): warning ASP0000: Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.

This doesn't appear to be an option - ConfigureApplicationCookie is in Startup.ConfigureServices and can only configure new services, Startup.Configure can use the new services, but can't override CookieAuthenticationOptions.SessionStore to be my custom store.

I've tried adding services.AddSingleton<ITicketStore>(p => new MyCustomRedisStore(cache, ...)) before ConfigureApplicationCookie, but this is ignored.

Explicitly setting CookieAuthenticationOptions.SessionStore appears to be the only way to get it to use anything other than the local memory store.

Every example I've found online uses BuildServiceProvider();

Ideally I want to do something like:

services.ConfigureApplicationCookieStore(provider => 
{
    var cache = provider.GetService<IDistributedCache>();
    return new MyCustomStore(cache, ...);
});

Or

public void Configure(IApplicationBuilder app, ... IDistributedCache cache)
{
    app.UseApplicationCookieStore(new MyCustomStore(cache, ...));
}

And then CookieAuthenticationOptions.SessionStore should just use whatever I've configured there.

How do I make the application cookie use an injected store?

Keith
  • 150,284
  • 78
  • 298
  • 434

2 Answers2

10

Reference Use DI services to configure options

If all the dependencies of your custom store are injectable, then just register your store and required dependencies with the service collection and use DI services to configure options

public void ConfigureServices(IServiceCollection services) {
    // Set up Redis distributed cache
    services.AddStackExchangeRedisCache(...);

    //register my custom store
    services.AddSingleton<ITicketStore, MyCustomRedisStore>();

    //...

    //Use DI services to configure options
    services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
        .Configure<ITicketStore>((options, store) => {
            options.SessionStore = store;
        });

    services.ConfigureApplicationCookie(options => {
        //do nothing
    }):
}

If not then work around what is actually registered

For example

//Use DI services to configure options
services.AddOptions<CookieAuthenticationOptions>(IdentityConstants.ApplicationScheme)
    .Configure<IDistributedCache>((options, cache) => {
        options.SessionStore = new MyCustomRedisStore(cache, ...);
    });

Note:

ConfigureApplicationCookie uses a named options instance. - @KirkLarkin

public static IServiceCollection ConfigureApplicationCookie(this IServiceCollection services, Action<CookieAuthenticationOptions> configure)
        => services.Configure(IdentityConstants.ApplicationScheme, configure);

The option would need to include the name when adding it to services.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Hrm, spoke too soon `.AddOptions()` seems to be the way, but it doesn't hit the code inside the `.Configure` method. – Keith Feb 25 '20 at 13:18
  • @Keith hmm. interesting. Try switching the order. Could be that the second call is overriding the first. – Nkosi Feb 25 '20 at 13:24
  • That doesn't seem to make a difference, and neither does what I pass to `Configure`. I think `AddOptions` is just ignored, possibly due to whatever the implementation of `ConfigureApplicationCookie` is. – Keith Feb 25 '20 at 13:31
  • 2
    `ConfigureApplicationCookie` uses a *named* options instance. I think you can use something like `services.AddOptions(IdentityConstants.ApplicationScheme)`... – Kirk Larkin Feb 25 '20 at 13:37
5

To implement Redis Tickets in .NET Core 3.0 we did the following which is the above in a bit more of a final form::

services.AddSingleton<ITicketStore, RedisTicketStore>();
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
     .Configure<ITicketStore>((options, store) => {
         options.SessionStore = store;
     });


services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
    .AddIdentityServerAuthentication(options =>
    {
           // ...configure identity server options
    }).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);

Here is a Redis implementation:

public class RedisTicketStore : ITicketStore
{
    private const string KeyPrefix = "AuthSessionStore-";
    private IDistributedCache cache;

    public RedisTicketStore(IDistributedCache cache)
    {
        this.cache = cache;
    }

    public async Task<string> 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)
    {
        var options = new DistributedCacheEntryOptions();
        var expiresUtc = ticket.Properties.ExpiresUtc;
        if (expiresUtc.HasValue)
        {
            options.SetAbsoluteExpiration(expiresUtc.Value);
        }
        byte[] val = SerializeToBytes(ticket);
        cache.Set(key, val, options);
        return Task.FromResult(0);
    }

    public Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        AuthenticationTicket ticket;
        byte[] bytes = null;
        bytes = cache.Get(key);
        ticket = DeserializeFromBytes(bytes);
        return Task.FromResult(ticket);
    }

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

    private static byte[] SerializeToBytes(AuthenticationTicket source)
    {
        return TicketSerializer.Default.Serialize(source);
    }

    private static AuthenticationTicket DeserializeFromBytes(byte[] source)
    {
        return source == null ? null : TicketSerializer.Default.Deserialize(source);
    }
}

Redis implementation from: https://mikerussellnz.github.io/.NET-Core-Auth-Ticket-Redis/

Joshua Enfield
  • 17,642
  • 10
  • 51
  • 98