diff --git a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs index 21ae878..3f7a265 100644 --- a/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Api/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using HopFrame.Api.Controller; using HopFrame.Api.Logic; using HopFrame.Api.Logic.Implementation; +using HopFrame.Api.Models; using HopFrame.Database; using HopFrame.Security.Authentication; using HopFrame.Security.Authentication.OpenID; @@ -18,9 +19,15 @@ public static class ServiceCollectionExtensions { /// /// The service provider to add the services to /// The configuration used to configure HopFrame authentication + /// Configuration for how the HopFrame services get set up /// The data source for all HopFrame entities - public static void AddHopFrame(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { - var controllers = new List { typeof(UserController), typeof(GroupController) }; + public static void AddHopFrame(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase { + config ??= new(); + + var controllers = new List(); + + if (config.ExposeModelEndpoints) + controllers.AddRange([typeof(UserController), typeof(GroupController)]); var defaultAuthenticationSection = configuration.GetSection("HopFrame:Authentication:DefaultAuthentication"); if (!defaultAuthenticationSection.Exists() || configuration.GetValue("HopFrame:Authentication:DefaultAuthentication")) @@ -31,29 +38,33 @@ public static class ServiceCollectionExtensions { controllers.Add(typeof(OpenIdController)); } - AddHopFrameNoEndpoints(services, configuration); + AddHopFrameNoEndpoints(services, configuration, config); services.AddMvcCore().UseSpecificControllers(controllers.ToArray()); } - + /// /// Adds all HopFrame services to the application /// /// The service provider to add the services to /// The configuration used to configure HopFrame authentication + /// Configuration for how the HopFrame services get set up /// The data source for all HopFrame entities - public static void AddHopFrameNoEndpoints(this IServiceCollection services, ConfigurationManager configuration) where TDbContext : HopDbContextBase { + public static void AddHopFrameNoEndpoints(this IServiceCollection services, ConfigurationManager configuration, HopFrameApiModuleConfig config = null) where TDbContext : HopDbContextBase { + config ??= new(); + services.AddMvcCore().ConfigureApplicationPartManager(manager => { var endpoints = manager.ApplicationParts.SingleOrDefault(p => p.Name == typeof(ServiceCollectionExtensions).Namespace!.Replace(".Extensions", "")); manager.ApplicationParts.Remove(endpoints); }); - + + services.AddSingleton(config); services.AddHopFrameRepositories(); services.TryAddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddHopFrameAuthentication(configuration); + services.AddHopFrameAuthentication(configuration, config); } } diff --git a/src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs b/src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs new file mode 100644 index 0000000..85afc5b --- /dev/null +++ b/src/HopFrame.Api/Models/HopFrameApiModuleConfig.cs @@ -0,0 +1,7 @@ +using HopFrame.Security.Models; + +namespace HopFrame.Api.Models; + +public class HopFrameApiModuleConfig : HopFrameConfig { + public bool ExposeModelEndpoints { get; set; } = true; +} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs index a6bf52c..228804e 100644 --- a/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs +++ b/src/HopFrame.Security/Authentication/HopFrameAuthenticationExtensions.cs @@ -3,6 +3,7 @@ using HopFrame.Security.Authentication.OpenID.Implementation; using HopFrame.Security.Authentication.OpenID.Options; using HopFrame.Security.Authorization; using HopFrame.Security.Claims; +using HopFrame.Security.Models; using HopFrame.Security.Options; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; @@ -18,8 +19,13 @@ public static class HopFrameAuthenticationExtensions { /// /// The service provider to add the services to /// The configuration used to configure HopFrame authentication + /// Configuration for how the HopFrame services get set up /// - public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration) { + public static IServiceCollection AddHopFrameAuthentication(this IServiceCollection service, ConfigurationManager configuration, HopFrameConfig config = null) { + config ??= new HopFrameConfig(); + + service.AddSingleton(config); + service.AddScoped(typeof(ICacheProvider), config.CacheProvider); service.TryAddSingleton(); service.AddScoped(); diff --git a/src/HopFrame.Security/Authentication/OpenID/ICacheProvider.cs b/src/HopFrame.Security/Authentication/OpenID/ICacheProvider.cs new file mode 100644 index 0000000..65a553b --- /dev/null +++ b/src/HopFrame.Security/Authentication/OpenID/ICacheProvider.cs @@ -0,0 +1,6 @@ +namespace HopFrame.Security.Authentication.OpenID; + +public interface ICacheProvider { + Task GetOrCreate(string key, Func> factory) where TItem : class; + Task Set(string key, TItem value, TimeSpan ttl); +} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/OpenID/Implementation/MemoryCacheProvider.cs b/src/HopFrame.Security/Authentication/OpenID/Implementation/MemoryCacheProvider.cs new file mode 100644 index 0000000..2b8ac3e --- /dev/null +++ b/src/HopFrame.Security/Authentication/OpenID/Implementation/MemoryCacheProvider.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Caching.Memory; + +namespace HopFrame.Security.Authentication.OpenID.Implementation; + +public class MemoryCacheProvider(IMemoryCache cache) : ICacheProvider { + public Task GetOrCreate(string key, Func> factory) where TItem : class { + if (cache.TryGetValue(key, out var value)) { + return Task.FromResult(value as TItem); + } + + return factory.Invoke(); + } + + public Task Set(string key, TItem value, TimeSpan ttl) { + cache.Set(key, value, ttl); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs b/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs index 2839d10..7aa1923 100644 --- a/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs +++ b/src/HopFrame.Security/Authentication/OpenID/Implementation/OpenIdAccessor.cs @@ -3,21 +3,24 @@ using HopFrame.Security.Authentication.OpenID.Models; using HopFrame.Security.Authentication.OpenID.Options; using HopFrame.Security.Claims; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; namespace HopFrame.Security.Authentication.OpenID.Implementation; -internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions options, IHttpContextAccessor accessor, IMemoryCache cache) : IOpenIdAccessor { +internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions options, IHttpContextAccessor accessor, ICacheProvider cache) : IOpenIdAccessor { private const string ConfigurationCacheKey = "HopFrame:OpenID:Configuration"; private const string AuthCodeCacheKey = "HopFrame:OpenID:Code:"; private const string TokenCacheKey = "HopFrame:OpenID:Token:"; - public async Task LoadConfiguration() { - if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled && cache.TryGetValue(ConfigurationCacheKey, out object cachedConfiguration)) { - return cachedConfiguration as OpenIdConfiguration; + public Task LoadConfiguration() { + if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) { + return cache.GetOrCreate(ConfigurationCacheKey, LoadConfigurationInCache); } + return LoadConfigurationInCache(); + } + + internal async Task LoadConfigurationInCache() { var client = clientFactory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, Path.Combine(options.Value.Issuer, ".well-known/openid-configuration").Replace("\\", "/")); var response = await client.SendAsync(request); @@ -28,16 +31,20 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions(await response.Content.ReadAsStreamAsync()); if (options.Value.Cache.Enabled && options.Value.Cache.Configuration.Enabled) - cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan); + await cache.Set(ConfigurationCacheKey, config, options.Value.Cache.Configuration.TTL.ConstructTimeSpan); return config; } - public async Task RequestToken(string code) { - if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled && cache.TryGetValue(AuthCodeCacheKey + code, out object cachedToken)) { - return cachedToken as OpenIdToken; + public Task RequestToken(string code) { + if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) { + return cache.GetOrCreate(AuthCodeCacheKey + code, () => RequestTokenInCache(code)); } - + + return RequestTokenInCache(code); + } + + internal async Task RequestTokenInCache(string code) { var protocol = accessor.HttpContext!.Request.IsHttps ? "https" : "http"; var callback = options.Value.Callback ?? Path.Combine($"{protocol}://{accessor.HttpContext!.Request.Host.Value}", IOpenIdAccessor.DefaultCallback).Replace("\\", "/"); @@ -61,7 +68,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions(await response.Content.ReadAsStreamAsync()); if (options.Value.Cache.Enabled && options.Value.Cache.Auth.Enabled) - cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan); + await cache.Set(AuthCodeCacheKey + code, token, options.Value.Cache.Auth.TTL.ConstructTimeSpan); return token; } @@ -74,11 +81,15 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions InspectToken(string token) { - if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled && cache.TryGetValue(TokenCacheKey + token, out object cachedToken)) { - return cachedToken as OpenIdIntrospection; + public Task InspectToken(string token) { + if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) { + return cache.GetOrCreate(TokenCacheKey + token, () => InspectTokenInCache(token)); } - + + return InspectTokenInCache(token); + } + + internal async Task InspectTokenInCache(string token) { var configuration = await LoadConfiguration(); var client = clientFactory.CreateClient(); @@ -97,7 +108,7 @@ internal class OpenIdAccessor(IHttpClientFactory clientFactory, IOptions(await response.Content.ReadAsStreamAsync()); if (options.Value.Cache.Enabled && options.Value.Cache.Inspection.Enabled) - cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan); + await cache.Set(TokenCacheKey + token, introspection, options.Value.Cache.Inspection.TTL.ConstructTimeSpan); return introspection; } diff --git a/src/HopFrame.Security/Models/HopFrameConfig.cs b/src/HopFrame.Security/Models/HopFrameConfig.cs new file mode 100644 index 0000000..119b541 --- /dev/null +++ b/src/HopFrame.Security/Models/HopFrameConfig.cs @@ -0,0 +1,7 @@ +using HopFrame.Security.Authentication.OpenID.Implementation; + +namespace HopFrame.Security.Models; + +public class HopFrameConfig { + public Type CacheProvider { get; set; } = typeof(MemoryCacheProvider); +} \ No newline at end of file diff --git a/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs b/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs index 226b144..0ff5118 100644 --- a/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs +++ b/src/HopFrame.Web/Models/HopFrameWebModuleConfig.cs @@ -1,5 +1,7 @@ +using HopFrame.Security.Models; + namespace HopFrame.Web.Models; -public class HopFrameWebModuleConfig { +public class HopFrameWebModuleConfig : HopFrameConfig { public string AdminLoginPageUri { get; set; } = "/administration/login"; } \ No newline at end of file diff --git a/src/HopFrame.Web/ServiceCollectionExtensions.cs b/src/HopFrame.Web/ServiceCollectionExtensions.cs index f87dc4b..c6b62fa 100644 --- a/src/HopFrame.Web/ServiceCollectionExtensions.cs +++ b/src/HopFrame.Web/ServiceCollectionExtensions.cs @@ -14,18 +14,19 @@ namespace HopFrame.Web; public static class ServiceCollectionExtensions { public static IServiceCollection AddHopFrame(this IServiceCollection services, ConfigurationManager configuration, HopFrameWebModuleConfig config = null) where TDbContext : HopDbContextBase { + config ??= new HopFrameWebModuleConfig(); services.AddHttpClient(); services.AddHopFrameRepositories(); services.AddScoped(); services.AddTransient(); services.AddAdminContext(); - services.AddSingleton(config ?? new HopFrameWebModuleConfig()); + services.AddSingleton(config); // Component library's services.AddSweetAlert2(); services.AddBlazorStrap(); - services.AddHopFrameAuthentication(configuration); + services.AddHopFrameAuthentication(configuration, config); return services; }