Search code examples
c#asp.net-core.net-coreasp.net-core-mvc-2.0

Replace Cookie Authentication Handler Scheme Options at Runtime


If I add cookie authentication to my ASP.Net core application with cookie authentication options using the native dependency injection container. How then can I replace the authentication options at run-time, after startup? For example, if I want to change the cookie expiration while the app is running. I cannot figure out how to replace both the authentication handler with its options in order to affect the change.

Code at startup to add authentication:

public static IServiceCollection ConfigureOAuth(this IServiceCollection services)
{
    var appSettings = services.BuildServiceProvider().GetService<IOptions<AppSettings>>();

    return services.AddAuthentication(o =>
    {
        o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, (o) =>
    {
        o.ExpireTimeSpan = TimeSpan.FromHours(appSettings.Value.HostOptions.SessionLifespanHours);
    })
    .Services;
}

Code at run-time to replace authentication:

/// <summary>
/// Replace authentication options with new ones read from configuration. 
/// 1). Remove old services
/// 2.) Reload the configuration 
/// 3.) Add the authentication scheme with options read from the latest configuration
/// </summary>
private static void ReplaceServices(IServiceCollection services, IHostingEnvironment env)
{
    ClearServices(services);

    services.Configure<AppSettings>(StartupConfiguration.BuildConfigurationRoot(env).GetSection("App"));

    var provider = services.BuildServiceProvider();
    var appSettings = provider.GetService<IOptions<AppSettings>>();

    services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<CookieAuthenticationOptions>, PostConfigureCookieAuthenticationOptions>());
    services.AddScheme<CookieAuthenticationOptions, CookieAuthenticationHandler>(CookieAuthenticationDefaults.AuthenticationScheme, (o) =>
    {
        o.ExpireTimeSpan = TimeSpan.FromHours(appSettings.Value.HostOptions.SessionLifespanHours);
    });
}

/// <summary>
/// Clear stale dependencies: application settings configured from appsettings.json, 
/// authentication options and cookie authentication handler and options
/// </summary>
private static void ClearServices(IServiceCollection services)
{
    var staleTypes = new List<Type>
    {
        typeof(IConfigureOptions<AppSettings>),
        typeof(IConfigureOptions<AuthenticationOptions>),
        typeof(IPostConfigureOptions<CookieAuthenticationOptions>),
        typeof(IConfigureOptions<CookieAuthenticationOptions>),
        typeof(CookieAuthenticationHandler)
    };

    foreach (var staleType in staleTypes)
    {
        var staleService = services.FirstOrDefault(s => s.ServiceType.Equals(staleType));
        services.Remove(staleService);
    }
}

Solution

  • Asp.net core native configuration reloading can be a bit flaky. If services depend on application settings that change at run-time, you don't have to inject those settings as IOptions at startup. An alternative way would be to write your settings provider which reloads a cached copy of the settings when an event notification is received from file system watcher. This approach eliminates the need to make configuration a DI service and you no longer need to depend on the reload token. The flow would go as follows:

    1. Create a configuration provider service that encapsulates reading and storing of the apps settings. On init, it reads appsettings.json and caches an instance of IConfigurationRoot
    2. Create another service to encapsulate watching the file system for changes to the app settings. If it changed, notify the configuration provider using a simple pub/sub pattern with a FileChangeEvent. The config provider can update the config root and then trigger a ConfigChangeEvent once the config is refreshed.
    3. Any services they rely on live configuration, such as authentication options, can subscribe to the ConfigChangeEvent and update its settings based on what section it needs from the config root.

    The key here is creating an options service that an authentication handler can successfully use and always has the live value. To do this, you need to implement an IOptionsMonitor.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(o =>
        {
            o.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            o.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddSingleton<IOptionsMonitor<CookieAuthenticationOptions>, CookieAuthenticationConfigurator>()
        .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
    }
    

    The IOptionsMonitor implemenation:

    {
        internal class CookieAuthenticationConfigurator : IOptionsMonitor<CookieAuthenticationOptions>
        {
            private readonly FileConfigurationBuilder ConfigProvider;
            private readonly IHostingEnvironment Environment;
            private readonly IDataProtectionProvider DataProtectionProvider;
            private readonly IMessageHub Hub;
    
            public CookieAuthenticationConfigurator(FileConfigurationBuilder configProvider, IDataProtectionProvider dataProtectionProvider, IMessageHub hub, IHostingEnvironment environment)
            {
                ConfigProvider = configProvider;
                Environment = environment;
                DataProtectionProvider = dataProtectionProvider;
                Hub = hub;
                Initialize();
            }
    
            private void Initialize()
            {
                Hub.Subscribe<ConfigurationChangeEvent>(_ =>
                {
                    Build();
                });
    
                Build();
            }
    
            private void Build()
            {
                var hostOptions = ConfigProvider.Get<HostOptions>("HostOptions");
                options = new CookieAuthenticationOptions
                {
                    ExpireTimeSpan = TimeSpan.FromHours(hostOptions.SessionLifespanHours)
                };
            }
    
            private CookieAuthenticationOptions options;
    
            public CookieAuthenticationOptions CurrentValue => options;
    
            public CookieAuthenticationOptions Get(string name)
            {
                PostConfigureCookieAuthenticationOptions op = new PostConfigureCookieAuthenticationOptions(DataProtectionProvider);
                op.PostConfigure(name, options);
                return options;
            }
    
            public IDisposable OnChange(Action<CookieAuthenticationOptions, string> listener)
            {
                throw new NotImplementedException();
            }
        }
    }