Reputation: 496
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);
}
}
Upvotes: 1
Views: 2668
Reputation: 496
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:
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();
}
}
}
Upvotes: 2