diff --git a/.gitignore b/.gitignore index a0a2cf4..412b422 100644 --- a/.gitignore +++ b/.gitignore @@ -332,4 +332,6 @@ ASALocalRun/ .mfractor/ /ReportGenerator /coverage/docs -coverage.* \ No newline at end of file +coverage.* + +*.xml \ No newline at end of file diff --git a/DymamicAuthProviders.sln b/DymamicAuthProviders.sln index e1ab368..3b57344 100644 --- a/DymamicAuthProviders.sln +++ b/DymamicAuthProviders.sln @@ -15,6 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .npmrc = .npmrc .releaserc.json = .releaserc.json appveyor.yml = appveyor.yml + appveyorinit.ps1 = appveyorinit.ps1 build.cmd = build.cmd build.ps1 = build.ps1 dotnet-install.ps1 = dotnet-install.ps1 @@ -38,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.AspNetCore.Authe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.AspNetCore.Authentication.TestBase", "src\Aguacongas.AspNetCore.Authentication.TestBase\Aguacongas.AspNetCore.Authentication.TestBase.csproj", "{B3DEE02F-9CD3-4009-AFEE-00F0B09811EC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.AspNetCore.Authentication.Redis", "src\Aguacongas.AspNetCore.Authentication.Redis\Aguacongas.AspNetCore.Authentication.Redis.csproj", "{CE9FE264-6139-47BF-970C-D1BBFCD7EFEA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aguacongas.AspNetCore.Authentication.Redis.Test", "test\Aguacongas.AspNetCore.Authentication.Redis.Test\Aguacongas.AspNetCore.Authentication.Redis.Test.csproj", "{87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +73,14 @@ Global {B3DEE02F-9CD3-4009-AFEE-00F0B09811EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3DEE02F-9CD3-4009-AFEE-00F0B09811EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {B3DEE02F-9CD3-4009-AFEE-00F0B09811EC}.Release|Any CPU.Build.0 = Release|Any CPU + {CE9FE264-6139-47BF-970C-D1BBFCD7EFEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE9FE264-6139-47BF-970C-D1BBFCD7EFEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE9FE264-6139-47BF-970C-D1BBFCD7EFEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE9FE264-6139-47BF-970C-D1BBFCD7EFEA}.Release|Any CPU.Build.0 = Release|Any CPU + {87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -79,6 +92,8 @@ Global {8CC87457-C579-4E39-A1C4-BAADADF500DE} = {245AA4AB-ECF4-4C69-B63D-C59D4B3F918C} {01E5055D-BF6A-435C-8858-F63A8FAC6D86} = {8DE53FA4-6C58-4FE4-840C-8CD5D5370324} {B3DEE02F-9CD3-4009-AFEE-00F0B09811EC} = {245AA4AB-ECF4-4C69-B63D-C59D4B3F918C} + {CE9FE264-6139-47BF-970C-D1BBFCD7EFEA} = {245AA4AB-ECF4-4C69-B63D-C59D4B3F918C} + {87CFA3EF-2A15-4861-A6DC-C74E9CA3EBCE} = {8DE53FA4-6C58-4FE4-840C-8CD5D5370324} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {823D9407-93C0-443E-89EC-D9DBB006A195} diff --git a/README.md b/README.md index 2083651..9e79e40 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Nuget packages -------------- |Aguacongas.AspNetCore.Authentication|Aguacongas.AspNetCore.Authentication.EntityFramework|Aguacongas.AspNetCore.Authentication.TestBase| |:------:|:------:|:------:| -|[![][Aguacongas.AspNetCore.Authentication-badge]][Aguacongas.AspNetCore.Authentication-nuget]|[![][Aguacongas.AspNetCore.Authentication.EntityFramework-badge]][Aguacongas.AspNetCore.Authentication.EntityFramework-nuget]| -|[![][Aguacongas.AspNetCore.Authentication.TestBase-downloadbadge]][Aguacongas.AspNetCore.Authentication.TestBase-nuget]| +|[![][Aguacongas.AspNetCore.Authentication-badge]][Aguacongas.AspNetCore.Authentication-nuget]|[![][Aguacongas.AspNetCore.Authentication.EntityFramework-badge]][Aguacongas.AspNetCore.Authentication.EntityFramework-nuget]|[![][Aguacongas.AspNetCore.Authentication.TestBase-badge]][Aguacongas.AspNetCore.Authentication.TestBase-nuget]| +|[![][Aguacongas.AspNetCore.Authentication-downloadbadge]][Aguacongas.AspNetCore.Authentication-nuget]|[![][Aguacongas.AspNetCore.Authentication.EntityFramework-downloadbadge]][Aguacongas.AspNetCore.Authentication.EntityFramework-nuget]|[![][Aguacongas.AspNetCore.Authentication.TestBase-downloadbadge]][Aguacongas.AspNetCore.Authentication.TestBase-nuget]| [Aguacongas.AspNetCore.Authentication-badge]: https://img.shields.io/nuget/v/Aguacongas.AspNetCore.Authentication.svg [Aguacongas.AspNetCore.Authentication-downloadbadge]: https://img.shields.io/nuget/dt/Aguacongas.AspNetCore.Authentication.svg @@ -114,25 +114,4 @@ And in the `Configure` method load the configuration with `LoadDynamicAuthentica ``` -## Extends - -You can implement your own store by implementing `IDynamicProviderStore` interface. -To verify your implementation you can override `DynamicManagerTestBase` of `Aguacongas.AspNetCore.Authentication.TestBase`. - -``` csharp -public class DynamicManagerTest: DynamicManagerTestBase -{ - public DynamicManagerTest(ITestOutputHelper output): base(output) - { - } - - protected override DynamicAuthenticationBuilder AddStore(DynamicAuthenticationBuilder builder) - { - builder.Services.AddDbContext(options => - { - options.UseInMemoryDatabase(Guid.NewGuid().ToString()); - }); - return builder.AddEntityFrameworkStore(); - } -} -``` \ No newline at end of file +Read the [wiki](../../wiki) for more information. diff --git a/appveyor.yml b/appveyor.yml index e522b4b..306293e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,8 @@ version: 1.0.{build} skip_tags: true -stack: node 9 +stack: node 9, redis skip_commits: - message: /\[chore\]/ + message: /^chore(release)/ branches: only: - master @@ -26,6 +26,9 @@ install: - sh: nuget install GitVersion.CommandLine -ExcludeVersion - sh: mono GitVersion.CommandLine/tools/GitVersion.exe /l console /output buildserver - ps: if ($isWindows) { .\dotnet-install.ps1 -Version 2.2.100 } + - cmd: nuget install redis-64 -excludeversion + - cmd: redis-64\tools\redis-server.exe --service-install + - cmd: redis-64\tools\redis-server.exe --service-start - cmd: gitversion /l console /output buildserver - cmd: nuget install ReportGenerator -ExcludeVersion - ps: ./appveyorinit.ps1 @@ -49,7 +52,7 @@ deploy: auth_token: $(GH_TOKEN) draft: true prerelease: true - release: $(APPVEYOR_BUILD_VERSION) + release: $(Version) on: branch: - /preview*/ @@ -83,10 +86,10 @@ for: - cmd: git config --global user.name "Aguacongas" - cmd: git checkout gh-pages - cmd: git stash - - cmd: mkdir %APPVEYOR_BUILD_VERSION% - - cmd: move coverage\docs %APPVEYOR_BUILD_VERSION% - - cmd: git add %APPVEYOR_BUILD_VERSION% - - cmd: git commit %APPVEYOR_BUILD_VERSION% -m "Appveyor build succed %APPVEYOR_BUILD_NUMBER%" + - cmd: mkdir %Version% + - cmd: move coverage\docs %Version% + - cmd: git add %Version% + - cmd: git commit %Version% -m "Appveyor build succed %APPVEYOR_BUILD_NUMBER%" - cmd: git push - branches: diff --git a/appveyorinit.ps1 b/appveyorinit.ps1 index c419e64..eb6590d 100644 --- a/appveyorinit.ps1 +++ b/appveyorinit.ps1 @@ -14,14 +14,14 @@ else } appveyor SetVariable -Name SemVer -Value $nextversion +appveyor AddMessage "SemVer = $nextversion" if (![string]::IsNullOrEmpty($env:GitVersion_PreReleaseLabel)) { $nextversion = "$nextversion-$env:GitVersion_PreReleaseLabel$env:GitVersion_CommitsSinceVersionSourcePadded" } - +appveyor SetVariable -Name Version -Value $nextversion appveyor UpdateBuild -Version $nextversion -$builnumbersuffix = Get-Date -Format "mmddyyyy-HHmm" -$builnumber = "$builnumber-$builnumbersuffix" +appveyor AddMessage "Version = $nextversion" dotnet restore diff --git a/sample/Aguacongas.AspNetCore.Authentication.Sample/Aguacongas.AspNetCore.Authentication.Sample.csproj b/sample/Aguacongas.AspNetCore.Authentication.Sample/Aguacongas.AspNetCore.Authentication.Sample.csproj index f1bfac0..f6a2d75 100644 --- a/sample/Aguacongas.AspNetCore.Authentication.Sample/Aguacongas.AspNetCore.Authentication.Sample.csproj +++ b/sample/Aguacongas.AspNetCore.Authentication.Sample/Aguacongas.AspNetCore.Authentication.Sample.csproj @@ -18,11 +18,7 @@ - - - - - + diff --git a/sample/Aguacongas.AspNetCore.Authentication.Sample/Controllers/HomeController.cs b/sample/Aguacongas.AspNetCore.Authentication.Sample/Controllers/HomeController.cs index 4e7334b..e02a67b 100644 --- a/sample/Aguacongas.AspNetCore.Authentication.Sample/Controllers/HomeController.cs +++ b/sample/Aguacongas.AspNetCore.Authentication.Sample/Controllers/HomeController.cs @@ -30,6 +30,7 @@ public IActionResult Index() return View(_manager.ManagedHandlerType.Select(t => t.Name)); } + // Returns an empty details view to create a scheme for a type of handler [Route("Create/{type}")] public IActionResult Create(string type) { @@ -39,6 +40,7 @@ public IActionResult Create(string type) }); } + // Creates a new scheme [HttpPost] [Route("Create/{type}")] public async Task Create(AuthenticationViewModel model) @@ -72,6 +74,7 @@ await _manager.AddAsync(new SchemeDefinition return View(model); } + // Returns a scheme details view to update id [Route("Update/{scheme}")] public async Task Update(string scheme) { @@ -79,18 +82,7 @@ public async Task Update(string scheme) var definition = await _manager.FindBySchemeAsync(scheme); if (definition == null) { - var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); - var authenticationScheme = schemes.FirstOrDefault(s => s.Name == scheme); - if (authenticationScheme == null) - { - return NotFound(); - } - model = new AuthenticationViewModel - { - Scheme = authenticationScheme.Name, - DisplayName = authenticationScheme.DisplayName, - HandlerType = authenticationScheme.HandlerType.Name - }; + return NotFound(); } else { @@ -101,20 +93,15 @@ public async Task Update(string scheme) HandlerType = definition.HandlerType.Name }; - if (definition.Options is OAuthOptions oAuthOptions) // GoogleOptions is OAuthOptions - { - model.ClientId = oAuthOptions.ClientId; - model.ClientSecret = oAuthOptions.ClientSecret; - } - else - { - return Error(); - } + var oAuthOptions = definition.Options as OAuthOptions; // GoogleOptions is OAuthOptions + model.ClientId = oAuthOptions.ClientId; + model.ClientSecret = oAuthOptions.ClientSecret; } return View(model); } + // Updates a scheme [HttpPost] [Route("Update/{scheme}")] public async Task Update(AuthenticationViewModel model) @@ -124,15 +111,13 @@ public async Task Update(AuthenticationViewModel model) var definition = await _manager.FindBySchemeAsync(model.Scheme); if (definition == null) { - await Create(model); - - return View(model); + return NotFound(); } if (definition.Options is OAuthOptions oAuthOptions) // GoogleOptions is OAuthOptions { oAuthOptions.ClientId = model.ClientId; - oAuthOptions.ClientSecret = model.ClientSecret; + oAuthOptions.ClientSecret = model.ClientSecret; } definition.DisplayName = model.DisplayName; @@ -143,6 +128,7 @@ public async Task Update(AuthenticationViewModel model) return View(model); } + // Lists all schemes we can manage [Route("List")] public async Task List() { @@ -160,9 +146,16 @@ public async Task List() })); } + // Deletes a scheme [Route("Delete/{scheme}")] public async Task Delete(string scheme) { + var definition = await _manager.FindBySchemeAsync(scheme); + if (definition == null) + { + return NotFound(); + } + await _manager.RemoveAsync(scheme); return RedirectToAction("List"); } diff --git a/sample/Aguacongas.AspNetCore.Authentication.Sample/SchemeChangeSubscriberSample.cs b/sample/Aguacongas.AspNetCore.Authentication.Sample/SchemeChangeSubscriberSample.cs new file mode 100644 index 0000000..d87285b --- /dev/null +++ b/sample/Aguacongas.AspNetCore.Authentication.Sample/SchemeChangeSubscriberSample.cs @@ -0,0 +1,54 @@ +using System.Threading.Tasks; + +namespace Aguacongas.AspNetCore.Authentication.Sample +{ + /// + /// Subsribe to scheme change notifications + /// + public class SchemeChangeSubscriber + { + private readonly NoPersistentDynamicManager _manager; + private readonly IDynamicProviderStore _store; + + public SchemeChangeSubscriber(NoPersistentDynamicManager manager, IDynamicProviderStore store) + { + _manager = manager; + _store = store; + } + + /// + /// Called when scheme change. + /// + /// The change. + /// + public async Task OnSchemeChange(SchemeChange change) + { + var definition = await _store.FindBySchemeAsync(change.Scheme); + switch(change.Kind) + { + case SchemeChangeKind.Added: + await _manager.AddAsync(definition); + break; + case SchemeChangeKind.Updated: + await _manager.UpdateAsync(definition); + break; + case SchemeChangeKind.Deleted: + await _manager.RemoveAsync(change.Scheme); + break; + } + } + } + + public enum SchemeChangeKind + { + Added, + Updated, + Deleted + } + + public class SchemeChange + { + public string Scheme { get; set; } + public SchemeChangeKind Kind { get; set; } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.csproj b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.csproj index a8ca42c..954bee9 100644 --- a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.csproj +++ b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.csproj @@ -22,6 +22,10 @@ C:\Projects\Perso\DymamicAuthProviders\src\Aguacongas.AspNetCore.Authentication.EntityFramework\Aguacongas.AspNetCore.Authentication.EntityFramework.xml + + + + diff --git a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.xml b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.xml deleted file mode 100644 index c7452f1..0000000 --- a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/Aguacongas.AspNetCore.Authentication.EntityFramework.xml +++ /dev/null @@ -1,208 +0,0 @@ - - - - Aguacongas.AspNetCore.Authentication.EntityFramework - - - - - IApplicationBuilder extensions - - - - - Loads the dynamic authentication configuration. - - The builder. - - - - - extensions. - - - - - Adds the entity framework store. - - The type of the context. - The builder. - The - - - - Implement a store for with EntityFramework. - - - - - - Initializes a new instance of the class. - - The context. - The authentication scheme options serializer. - The logger. - - - - Implement a store for with EntityFramework. - - The type of the definition. - - - - - Initializes a new instance of the class. - - The context. - The authentication scheme options serializer. - The logger. - - - - Implement a store for with EntityFramework. - - The type of the definition. - The type of the context. - - - - - Gets the scheme definitions list. - - - The scheme definitions list. - - - - - Initializes a new instance of the class. - - The context. - The authentication scheme options serializer. - The logger. - - context - or - authenticationSchemeOptionsSerializer - or - logger - - - - - Adds a defnition asynchronously. - - The definition. - The cancellation token. - - definition - - - - Removes a scheme definition asynchronous. - - The definition. - The cancellation token. - - definition - - - - Updates a scheme definition asynchronous. - - The definition. - The cancellation token. - - definition - - - - Finds scheme definition by scheme asynchronous. - - The scheme. - The cancellation token. - - An instance of TSchemeDefinition or null. - - Parameter {nameof(scheme)} - - - - Scheme definition db context of - - - - - - - Initializes a new instance of the class. - - The options for this context. - - - - Scheme definition db context - - The type of the scheme otptions. - - - - Gets or sets the providers. - - - The providers. - - - - - Initializes a new instance of the class. - - The options for this context. - - - - Override this method to further configure the model that was discovered by convention from the entity types - exposed in properties on your derived context. The resulting model may be cached - and re-used for subsequent instances of your derived context. - - The builder being used to construct the model for this context. Databases (and other extensions) typically - define extension methods on this object that allow you to configure aspects of the model that are specific - to a given database. - - If a model is explicitly set on the options for this context (via ) - then this method will not be run. - - - - - Scheme definition for entity framework store - - - - - - Gets or sets the serialized handler type. - - - The name of the serialized handler type. - - - - - Gets or sets the serialized options. - - - The serialized options. - - - - - Gets or sets the concurrency stamp. - - - The concurrency stamp. - - - - diff --git a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/ApplicationBuilderExtensions.cs b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/ApplicationBuilderExtensions.cs deleted file mode 100644 index 4a93f92..0000000 --- a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/ApplicationBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Project: aguacongas/DymamicAuthProviders -// Copyright (c) 2018 @Olivier Lefebvre -using Aguacongas.AspNetCore.Authentication; - -namespace Microsoft.AspNetCore.Builder -{ - /// - /// IApplicationBuilder extensions - /// - public static class ApplicationBuilderExtensions - { - /// - /// Loads the dynamic authentication configuration. - /// - /// The builder. - /// - public static IApplicationBuilder LoadDynamicAuthenticationConfiguration(this IApplicationBuilder builder) - { - builder.ApplicationServices.LoadDynamicAuthenticationConfiguration(); - return builder; - } - - } -} diff --git a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/DynamicAuthenticationBuilderExtensions.cs b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/DynamicAuthenticationBuilderExtensions.cs index 82f877f..c8be1c0 100644 --- a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/DynamicAuthenticationBuilderExtensions.cs +++ b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/DynamicAuthenticationBuilderExtensions.cs @@ -2,11 +2,9 @@ // Copyright (c) 2018 @Olivier Lefebvre using Aguacongas.AspNetCore.Authentication; using Aguacongas.AspNetCore.Authentication.EntityFramework; -using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection.Extensions; using System; -using System.Reflection; namespace Microsoft.Extensions.DependencyInjection { @@ -36,21 +34,5 @@ private static void AddStore(IServiceCollection service, Type definitionType, Ty service.AddTransient(); } - - private static TypeInfo FindGenericBaseType(Type currentType, Type genericBaseType) - { - var type = currentType; - while (type != null) - { - var typeInfo = type.GetTypeInfo(); - var genericType = type.IsGenericType ? type.GetGenericTypeDefinition() : null; - if (genericType != null && genericType == genericBaseType) - { - return typeInfo; - } - type = type.BaseType; - } - return null; - } } } diff --git a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/SchemeDbContext.cs b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/SchemeDbContext.cs index 6f9c4e1..1fc0e79 100644 --- a/src/Aguacongas.AspNetCore.Authentication.EntityFramework/SchemeDbContext.cs +++ b/src/Aguacongas.AspNetCore.Authentication.EntityFramework/SchemeDbContext.cs @@ -56,10 +56,14 @@ public SchemeDbContext(DbContextOptions options): base(options) /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity() - .Ignore(p => p.Options) - .Ignore(p => p.HandlerType) - .HasKey(p => p.Scheme); + modelBuilder.Entity(b => + { + b.Ignore(p => p.Options) + .Ignore(p => p.HandlerType) + .HasKey(p => p.Scheme); + b.Property(p => p.ConcurrencyStamp).IsConcurrencyToken(); + }); + } } } diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/Aguacongas.AspNetCore.Authentication.Redis.csproj b/src/Aguacongas.AspNetCore.Authentication.Redis/Aguacongas.AspNetCore.Authentication.Redis.csproj new file mode 100644 index 0000000..7996fd2 --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/Aguacongas.AspNetCore.Authentication.Redis.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicAuthenticationBuilderExtensions.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicAuthenticationBuilderExtensions.cs new file mode 100644 index 0000000..0624368 --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicAuthenticationBuilderExtensions.cs @@ -0,0 +1,142 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre +using Aguacongas.AspNetCore.Authentication; +using Aguacongas.AspNetCore.Authentication.Redis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using System; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// extensions. + /// + public static class DynamicAuthenticationBuilderExtensions + { + /// + /// Adds an Redis implementation of identity stores. + /// + /// The type of scheme definition. + /// The instance this method extends. + /// Action to configure + /// (Optional) The redis database to use + /// The instance this method extends. + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, Action configure, int? database = null) + { + return builder.AddRedisStore(configure, database); + } + + /// + /// Adds an Redis implementation of identity stores. + /// + /// The type of scheme definition. + /// The instance this method extends. + /// Action to configure + /// (Optional) The redis database to use + /// The instance this method extends. + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, Action configure, int? database = null) + where TSchemeDefinition : SchemeDefinition, new() + { + var services = builder.Services; + + services.Configure(configure) + .AddSingleton(provider => + { + var options = provider.GetRequiredService>().Value; + var redisLogger = CreateLogger(provider); + return ConnectionMultiplexer.Connect(options, redisLogger); + }); + + return builder.AddRedisStore(provider => + { + var options = provider.GetRequiredService>().Value; + var multiplexer = provider.GetRequiredService(); + return multiplexer.GetDatabase(database ?? (options.DefaultDatabase ?? -1)); + }); + } + + /// + /// Adds an Redis implementation of identity stores. + /// + /// The instance this method extends. + /// The redis configuration string + /// (Optional) The redis database to use + /// (Optional) a to write log + /// The instance this method extends. + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, string configuration, int? database = null) + { + return builder.AddRedisStore(configuration, database); + } + + /// + /// Adds an Redis implementation of identity stores. + /// + /// The type of scheme definition. + /// The instance this method extends. + /// The redis configuration string + /// (Optional) The redis database to use + /// (Optional) a to write log + /// The instance this method extends. + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, string configuration, int? database = null) + where TSchemeDefinition : SchemeDefinition, new() + { + var services = builder.Services; + + services.AddSingleton(provider => + { + var redisLogger = CreateLogger(provider); + + return ConnectionMultiplexer.Connect(configuration, redisLogger); + }); + + return builder + .AddRedisStore(provider => + { + var multiplexer = provider.GetRequiredService(); + return multiplexer.GetDatabase(database ?? -1); + }); + } + + /// + /// Adds the entity framework store. + /// + /// The builder. + /// A function returning a + /// The + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, Func getDatabase) + { + return builder.AddRedisStore(getDatabase); + } + + /// + /// Adds the redis store. + /// + /// The type of scheme definition. + /// The builder. + /// A function returning a + /// The + public static DynamicAuthenticationBuilder AddRedisStore(this DynamicAuthenticationBuilder builder, Func getDatabase) + where TSchemeDefinition: SchemeDefinition, new() + { + var services = builder.Services; + + services.AddTransient, RedisAuthenticationSchemeOptionsSerializer>(); + services.AddTransient>(provider => + { + var db = getDatabase(provider); + var serializer = provider.GetRequiredService>(); + var logger = provider.GetRequiredService>>(); + + return new DynamicProviderStore(db, serializer, logger); + }); + return builder; + } + private static RedisLogger CreateLogger(IServiceProvider provider) + { + var logger = provider.GetService>(); + var redisLogger = logger != null ? new RedisLogger(logger) : null; + return redisLogger; + } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicProviderStore.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicProviderStore.cs new file mode 100644 index 0000000..8cd4b5d --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/DynamicProviderStore.cs @@ -0,0 +1,193 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre + +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Aguacongas.AspNetCore.Authentication.Redis +{ + /// + /// Implement a store for with EntityFramework. + /// + /// + public class DynamicProviderStore : DynamicProviderStore + { + /// + /// Initializes a new instance of the class. + /// + /// The Redis db. + /// The authentication scheme options serializer. + /// The logger. + public DynamicProviderStore(IDatabase db, IRedisAuthenticationSchemeOptionsSerializer authenticationSchemeOptionsSerializer, ILogger logger) : base(db, authenticationSchemeOptionsSerializer, logger) + { + } + } + + /// + /// Implement a store for with EntityFramework. + /// + /// The type of the definition. + /// + public class DynamicProviderStore : IDynamicProviderStore + where TSchemeDefinition : SchemeDefinition, new() + { + public const string StoreKey = "schemes"; + public const string ConcurencyKey = "schemes-concurency"; + + private readonly IDatabase _db; + private readonly IRedisAuthenticationSchemeOptionsSerializer _authenticationSchemeOptionsSerializer; + private readonly ILogger> _logger; + + + public IQueryable SchemeDefinitions => _db.HashGetAll(StoreKey) + .Select(entry => _authenticationSchemeOptionsSerializer.Deserialize(entry.Value)) + .AsQueryable(); + + /// + /// Initializes a new instance of the class. + /// + /// The Redis db. + /// The authentication scheme options serializer. + /// The logger. + /// + /// db + /// or + /// authenticationSchemeOptionsSerializer + /// or + /// logger + /// + public DynamicProviderStore(IDatabase db, IRedisAuthenticationSchemeOptionsSerializer authenticationSchemeOptionsSerializer, ILogger> logger) + { + _db = db ?? throw new ArgumentNullException(nameof(db)); + _authenticationSchemeOptionsSerializer = authenticationSchemeOptionsSerializer ?? throw new ArgumentNullException(nameof(authenticationSchemeOptionsSerializer)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Adds a defnition asynchronously. + /// + /// The definition. + /// The cancellation token. + /// + /// definition + public virtual async Task AddAsync(TSchemeDefinition definition, CancellationToken cancellationToken = default(CancellationToken)) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + var tran = _db.CreateTransaction(); + var notExistsCondition = tran.AddCondition(Condition.HashNotExists(StoreKey, definition.Scheme)); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + tran.HashSetAsync(StoreKey, + definition.Scheme, + _authenticationSchemeOptionsSerializer.Serialize(definition)); + tran.HashSetAsync(ConcurencyKey, definition.Scheme, 0); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + var result = await tran.ExecuteAsync(); + if (!result) + { + throw new InvalidOperationException($"The scheme {definition.Scheme} already exists"); + } + + _logger.LogInformation("Scheme {scheme} added for {handlerType} with options: {options}", definition.Scheme, definition.HandlerType, definition.SerializedOptions); + } + + /// + /// Finds scheme definition by scheme asynchronous. + /// + /// The scheme. + /// The cancellation token. + /// + /// An instance of TSchemeDefinition or null. + /// + /// Parameter {nameof(scheme)} + public virtual async Task FindBySchemeAsync(string scheme, CancellationToken cancellationToken = default(CancellationToken)) + { + if (string.IsNullOrWhiteSpace(scheme)) + { + throw new ArgumentException($"Parameter {nameof(scheme)} cannor be null or empty"); + } + + var value = await _db.HashGetAsync(StoreKey, scheme).ConfigureAwait(false); + if (value.HasValue) + { + var definition = _authenticationSchemeOptionsSerializer.Deserialize(value); + definition.ConcurrencyStamp = (long)await _db.HashGetAsync(ConcurencyKey, scheme); + return definition; + } + + return default(TSchemeDefinition); + } + + /// + /// Removes a scheme definition asynchronous. + /// + /// The definition. + /// The cancellation token. + /// + /// definition + public virtual async Task RemoveAsync(TSchemeDefinition definition, CancellationToken cancellationToken = default(CancellationToken)) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + var tran = _db.CreateTransaction(); + var concurrencyCondition = tran.AddCondition(Condition.HashEqual(ConcurencyKey, definition.Scheme, definition.ConcurrencyStamp)); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + tran.HashDeleteAsync(StoreKey, definition.Scheme); + tran.HashDeleteAsync(ConcurencyKey, definition.Scheme); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + var result = await tran.ExecuteAsync(); + if (!result) + { + throw new InvalidOperationException($"ConcurrencyStamp not match for scheme {definition.Scheme}"); + } + + _logger.LogInformation("Scheme {scheme} removed", definition.Scheme); + } + + /// + /// Updates a scheme definition asynchronous. + /// + /// The definition. + /// The cancellation token. + /// + /// definition + public virtual async Task UpdateAsync(TSchemeDefinition definition, CancellationToken cancellationToken = default(CancellationToken)) + { + if (definition == null) + { + throw new ArgumentNullException(nameof(definition)); + } + + definition.ConcurrencyStamp = 0; + + var tran = _db.CreateTransaction(); + var concurrencyCondition = tran.AddCondition(Condition.HashEqual(ConcurencyKey, definition.Scheme, definition.ConcurrencyStamp)); +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + tran.HashSetAsync(StoreKey, definition.Scheme, _authenticationSchemeOptionsSerializer.Serialize(definition)); + var concurency = tran.HashIncrementAsync(ConcurencyKey, definition.Scheme); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + var result = await tran.ExecuteAsync(); + if (!result) + { + throw new InvalidOperationException($"ConcurrencyStamp not match for scheme {definition.Scheme}"); + } + + definition.ConcurrencyStamp = concurency.Result; + + _logger.LogInformation("Scheme {scheme} updated for {handlerType} with options: {options}", definition.Scheme, definition.HandlerType, definition.SerializedOptions); + } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/IRedisAuthenticationSchemeOptionsSerializer.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/IRedisAuthenticationSchemeOptionsSerializer.cs new file mode 100644 index 0000000..0cd806c --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/IRedisAuthenticationSchemeOptionsSerializer.cs @@ -0,0 +1,9 @@ +namespace Aguacongas.AspNetCore.Authentication.Redis +{ + public interface IRedisAuthenticationSchemeOptionsSerializer + where TSchemeDefinition : SchemeDefinition + { + TSchemeDefinition Deserialize(string value); + string Serialize(TSchemeDefinition definition); + } +} \ No newline at end of file diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/RedisAuthenticationSchemeOptionsSerializer.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/RedisAuthenticationSchemeOptionsSerializer.cs new file mode 100644 index 0000000..b0fc638 --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/RedisAuthenticationSchemeOptionsSerializer.cs @@ -0,0 +1,34 @@ +using System; + +namespace Aguacongas.AspNetCore.Authentication.Redis +{ + public class RedisAuthenticationSchemeOptionsSerializer : AuthenticationSchemeOptionsSerializer, IRedisAuthenticationSchemeOptionsSerializer + where TSchemeDefinition: SchemeDefinition + { + public string Serialize(TSchemeDefinition definition) + { + var options = definition.Options; + var type = definition.HandlerType; + definition.HandlerType = null; + definition.Options = null; + + definition.SerializedHandlerType = SerializeType(type); + definition.SerializedOptions = SerializeOptions(options, type.GetAuthenticationSchemeOptionsType()); + + var result = Serialize(definition, typeof(TSchemeDefinition)); + + definition.HandlerType = type; + definition.Options = options; + + return result; + } + + public TSchemeDefinition Deserialize(string value) + { + var definition = base.Deserialize(value, typeof(TSchemeDefinition)) as TSchemeDefinition; + definition.HandlerType = DeserializeType(definition.SerializedHandlerType); + definition.Options = DeserializeOptions(definition.SerializedOptions, definition.HandlerType.GetAuthenticationSchemeOptionsType()); + return definition; + } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/RedisLogger.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/RedisLogger.cs new file mode 100644 index 0000000..c3978b4 --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/RedisLogger.cs @@ -0,0 +1,58 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Text; + +namespace Aguacongas.AspNetCore.Authentication.Redis +{ + /// + /// Redis Logger + /// + public class RedisLogger : TextWriter + { + private readonly ILogger _logger; + + public override Encoding Encoding => throw new NotImplementedException(); + + /// + /// Constructs a new instance of . + /// + /// A logger + public RedisLogger(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public override void WriteLine(string format, object arg0) + { + _logger.LogTrace(format, arg0); + } + + /// + public override void WriteLine(string format, object arg0, object arg1) + { + _logger.LogTrace(format, arg0, arg1); + } + + /// + public override void WriteLine(string format, object arg0, object arg1, object arg2) + { + _logger.LogTrace(format, arg0, arg1, arg2); + } + + /// + public override void WriteLine(string format, params object[] arg) + { + _logger.LogTrace(format, arg); + } + + /// + public override void WriteLine(string value) + { + _logger.LogTrace(value); + } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.Redis/SchemeDefinition.cs b/src/Aguacongas.AspNetCore.Authentication.Redis/SchemeDefinition.cs new file mode 100644 index 0000000..9b8c24b --- /dev/null +++ b/src/Aguacongas.AspNetCore.Authentication.Redis/SchemeDefinition.cs @@ -0,0 +1,35 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre +namespace Aguacongas.AspNetCore.Authentication.Redis +{ + /// + /// Scheme definition for entity framework store + /// + /// + public class SchemeDefinition : SchemeDefinitionBase + { + /// + /// Gets or sets the serialized handler type. + /// + /// + /// The name of the serialized handler type. + /// + public string SerializedHandlerType { get; set; } + + /// + /// Gets or sets the serialized options. + /// + /// + /// The serialized options. + /// + public string SerializedOptions { get; set; } + + /// + /// Gets or sets the concurrency stamp. + /// + /// + /// The concurrency stamp. + /// + public long ConcurrencyStamp { get; set; } + } +} diff --git a/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.csproj b/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.csproj index 2d2fd19..cfc26ab 100644 --- a/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.csproj +++ b/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.csproj @@ -20,6 +20,10 @@ C:\Projects\Perso\DymamicAuthProviders\src\Aguacongas.AspNetCore.Authentication.TestBase\Aguacongas.AspNetCore.Authentication.TestBase.xml + + + + diff --git a/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.xml b/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.xml deleted file mode 100644 index eceaed7..0000000 --- a/src/Aguacongas.AspNetCore.Authentication.TestBase/Aguacongas.AspNetCore.Authentication.TestBase.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - Aguacongas.AspNetCore.Authentication.TestBase - - - - - Base test suite to verify if the store implementation work as expecter - - The type of the scheme definition. - - - - Initializes a new instance of the class. - - The output. - - - diff --git a/src/Aguacongas.AspNetCore.Authentication.TestBase/DynamicManagerTestBase.cs b/src/Aguacongas.AspNetCore.Authentication.TestBase/DynamicManagerTestBase.cs index d6dc78d..e5d014c 100644 --- a/src/Aguacongas.AspNetCore.Authentication.TestBase/DynamicManagerTestBase.cs +++ b/src/Aguacongas.AspNetCore.Authentication.TestBase/DynamicManagerTestBase.cs @@ -16,7 +16,6 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.WsFederation; using Moq; -using Newtonsoft.Json; using System; using System.Net.Http; using System.Security.Claims; @@ -45,6 +44,46 @@ public DynamicManagerTestBase(ITestOutputHelper output) _output = output; } + /// + /// AddAsync should fail on suplicate scheme + /// + /// + [Fact] + public async Task AddAsync_should_fail_on_duplicate_scheme() + { + var provider = CreateServiceProvider(options => + { + options.AddCookie(); + }); + + var cookieOptions = new CookieAuthenticationOptions + { + Cookie = new CookieBuilder + { + Domain = "test" + } + }; + + var scheme = Guid.NewGuid().ToString(); + var definition = new TSchemeDefinition + { + Scheme = scheme, + DisplayName = "test", + HandlerType = typeof(CookieAuthenticationHandler), + Options = cookieOptions + }; + + var sut = provider.GetRequiredService>(); + + await sut.AddAsync(definition); + try + { + await sut.AddAsync(definition); + throw new InvalidOperationException("AddAsync should fail on ducplicate scheme"); + } + catch { } + } + /// /// AddAsync method should add cookie handler. /// @@ -78,15 +117,16 @@ Task onSignId(CookieSignedInContext context) } }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = CookieAuthenticationDefaults.AuthenticationScheme, + Scheme = scheme, DisplayName = "test", HandlerType = typeof(CookieAuthenticationHandler), Options = cookieOptions }; await sut.AddAsync(definition); - var state = await VerifyAddedAsync(CookieAuthenticationDefaults.AuthenticationScheme, provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnSignedIn(new CookieSignedInContext( @@ -129,16 +169,18 @@ Task onCreatingTicket(OAuthCreatingTicketContext context) AppId = "test", AppSecret = "test" }; + + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(FacebookHandler), Options = facebookOptions }; await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnCreatingTicket(new OAuthCreatingTicketContext( @@ -184,15 +226,16 @@ Task onCreatingTicket(OAuthCreatingTicketContext context) ClientSecret = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(GoogleHandler), Options = googleOptions }; await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnCreatingTicket(new OAuthCreatingTicketContext( @@ -236,9 +279,10 @@ Task onMessageReceived(Microsoft.AspNetCore.Authentication.JwtBearer.MessageRece RequireHttpsMetadata = false }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(JwtBearerHandler), Options = jwtBearerOptions @@ -248,7 +292,7 @@ Task onMessageReceived(Microsoft.AspNetCore.Authentication.JwtBearer.MessageRece Assert.Contains(typeof(JwtBearerHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnMessageReceived(new Microsoft.AspNetCore.Authentication.JwtBearer.MessageReceivedContext( @@ -287,9 +331,10 @@ Task onCreatingTicket(OAuthCreatingTicketContext context) ClientSecret = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(MicrosoftAccountHandler), Options = msAccountOptions @@ -299,7 +344,7 @@ Task onCreatingTicket(OAuthCreatingTicketContext context) Assert.Contains(typeof(MicrosoftAccountHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnCreatingTicket(new OAuthCreatingTicketContext( @@ -342,9 +387,10 @@ Task onTicketReceived(TicketReceivedContext context) ClientSecret = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(OpenIdConnectHandler), Options = oidcptions @@ -354,7 +400,7 @@ Task onTicketReceived(TicketReceivedContext context) Assert.Contains(typeof(OpenIdConnectHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnTicketReceived(new TicketReceivedContext( @@ -394,9 +440,10 @@ Task onTicketReceived(TicketReceivedContext context) ConsumerSecret = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(TwitterHandler), Options = twittertOptions @@ -406,7 +453,7 @@ Task onTicketReceived(TicketReceivedContext context) Assert.Contains(typeof(TwitterHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnTicketReceived(new TicketReceivedContext( @@ -436,9 +483,10 @@ public async Task AddAsync_should_ensure_uniq_callback_path() ConsumerSecret = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(TwitterHandler), Options = twittertOptions @@ -448,9 +496,9 @@ public async Task AddAsync_should_ensure_uniq_callback_path() Assert.Contains(typeof(TwitterHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); - definition.Scheme = "test2"; + definition.Scheme = Guid.NewGuid().ToString(); await Assert.ThrowsAsync(() => sut.AddAsync(definition)); } @@ -485,9 +533,10 @@ Task onTicketReceived(TicketReceivedContext context) } }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(WsFederationHandler), Options = wsFederationOptions @@ -497,14 +546,14 @@ Task onTicketReceived(TicketReceivedContext context) Assert.Contains(typeof(WsFederationHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnTicketReceived(new TicketReceivedContext( httpContext, state.scheme as AuthenticationScheme, state.options as WsFederationOptions, - new AuthenticationTicket(new ClaimsPrincipal(), "test"))); + new AuthenticationTicket(new ClaimsPrincipal(), scheme))); Assert.True(eventCalled); } @@ -536,9 +585,10 @@ Task onTicketReceived(TicketReceivedContext context) ClientId = "test" }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(FakeGenericHandler), Options = oAuthOptions @@ -548,14 +598,14 @@ Task onTicketReceived(TicketReceivedContext context) Assert.Contains(typeof(FakeGenericHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); var httpContext = new Mock().Object; state.options.Events.OnTicketReceived(new TicketReceivedContext( httpContext, state.scheme as AuthenticationScheme, state.options as OAuthOptions, - new AuthenticationTicket(new ClaimsPrincipal(), "test"))); + new AuthenticationTicket(new ClaimsPrincipal(), scheme))); Assert.True(eventCalled); } @@ -592,9 +642,10 @@ Task onTicketReceived(TicketReceivedContext context) } }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(CookieAuthenticationHandler), Options = cookieOptions @@ -607,7 +658,7 @@ Task onTicketReceived(TicketReceivedContext context) await Assert.ThrowsAsync(() => sut.UpdateAsync(definition)); await sut.AddAsync(definition); - await VerifyAddedAsync("test", provider); + await VerifyAddedAsync(scheme, provider); var wsFederationOptions = new WsFederationOptions { @@ -622,7 +673,7 @@ Task onTicketReceived(TicketReceivedContext context) definition.HandlerType = typeof(WsFederationHandler); await sut.UpdateAsync(definition); - var state = await VerifyAddedAsync("test", provider); + var state = await VerifyAddedAsync(scheme, provider); Assert.Equal(state.scheme.DisplayName, definition.DisplayName); @@ -631,7 +682,7 @@ Task onTicketReceived(TicketReceivedContext context) httpContext, state.scheme as AuthenticationScheme, state.options as WsFederationOptions, - new AuthenticationTicket(new ClaimsPrincipal(), "test"))); + new AuthenticationTicket(new ClaimsPrincipal(), scheme))); Assert.True(eventCalled); } @@ -656,9 +707,10 @@ public async Task RemoveAsync_should_remove_handler() } }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, DisplayName = "test", HandlerType = typeof(CookieAuthenticationHandler), Options = cookieOptions @@ -668,10 +720,10 @@ public async Task RemoveAsync_should_remove_handler() Assert.Contains(typeof(CookieAuthenticationHandler), sut.ManagedHandlerType); await sut.AddAsync(definition); - await VerifyAddedAsync("test", provider); + await VerifyAddedAsync(scheme, provider); - await sut.RemoveAsync("test"); - await Assert.ThrowsAsync(() => VerifyAddedAsync("test", provider)); + await sut.RemoveAsync(scheme); + await Assert.ThrowsAsync(() => VerifyAddedAsync(scheme, provider)); } /// @@ -696,9 +748,10 @@ public async Task Load_should_load_configuration() } }; + var scheme = Guid.NewGuid().ToString(); var definition = new TSchemeDefinition { - Scheme = "test", + Scheme = scheme, HandlerType = typeof(CookieAuthenticationHandler), Options = cookieOptions }; @@ -709,7 +762,7 @@ public async Task Load_should_load_configuration() sut.Load(); - await VerifyAddedAsync("test", provider); + await VerifyAddedAsync(scheme, provider); } /// @@ -735,7 +788,12 @@ private async Task VerifyAddedAsync(string schemeName, IServi return new { definition, scheme, options }; } - private IServiceProvider CreateServiceProvider(Action addHandlers = null) + /// + /// Creates the service providers + /// + /// A function to invoke before returning the provider + /// + protected virtual IServiceProvider CreateServiceProvider(Action addHandlers = null) { var services = new ServiceCollection(); var builder = services.AddLogging(configure => diff --git a/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.csproj b/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.csproj index e42fc48..bd7ee78 100644 --- a/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.csproj +++ b/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.csproj @@ -21,6 +21,10 @@ C:\Projects\Perso\DymamicAuthProviders\src\Aguacongas.AspNetCore.Authentication\Aguacongas.AspNetCore.Authentication.xml + + + + diff --git a/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.xml b/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.xml deleted file mode 100644 index 5589e7b..0000000 --- a/src/Aguacongas.AspNetCore.Authentication/Aguacongas.AspNetCore.Authentication.xml +++ /dev/null @@ -1,512 +0,0 @@ - - - - Aguacongas.AspNetCore.Authentication - - - - - IApplicationBuilder extensions to load configuration - - - - - Loads the dynamic authentication configuration. - - The type of the definition. - The builder. - - - - - Loads the dynamic authentication configuration. - - The type of the definition. - The provider. - - - - - AuthenticationBuilder extensions - - - - - Configures the DI for dynamic scheme management. - - The type of the definition. - The builder. - - - - - Manage serialization. - - - - - - Gets or sets the json serializer settings. - - - The json serializer settings. - - - - - Serializes the specified options. - - The options. - Type of the options. - - The serialized result. - - - - - Deserializes the specified value. - - The value. - Type of the options. - - An AuthenticationSchemeOptions instance. - - - - - Deserializes the type. - - The value. - - - - - Serializes the type. - - The type. - - - - - Serializes the specified value. - - The value. - The type. - - - - - Deserializes the specified value. - - The value. - The type. - - - - - Ignore delegate, interface and read-only property ContractResolver - - - - - Creates a for the given . - - - The member to create a for. - - - The member's parent . - - - A created for the given . - - - - - Configure the DI for dynamic scheme management. - - - - - - Gets the handler types managed by this instance. - - - The handler types. - - - - - Gets the type of the definition. - - - The type of the definition. - - - - - Initializes a new instance of the class. - - The services. - Type of the definition. - - - - Adds a based that supports remote authentication - which can be used by . - - The type to configure the handler."/>. - The used to handle this scheme. - The name of this scheme. - The display name of this scheme. - Used to configure the scheme options. - - The builder. - - - - - Adds a which can be used by . - - The type to configure the handler."/>. - The used to handle this scheme. - The name of this scheme. - The display name of this scheme. - Used to configure the scheme options. - - The builder. - - - - - Dynamic scheme manager which persist the changes. - - The type of the scheme definition. - - - - - Gets the scheme definitions list. - - - The scheme definitions list. - - - - - Initializes a new instance of the class. - - The scheme provider. - The wrapper factory. - The store. - The managed types. - store - - - - Adds a scheme asynchronously. - - The definition. - The cancellation token. - - - - - Removes the scheme asynchronous. - - The scheme. - The cancellation token. - - - - - Updates the scheme asynchronous. - - The definition. - The cancellation token. - - - - - Finds the definition by scheme asynchronous. - - The scheme. - The scheme definition or null. - scheme cannot be null or white space. - - - - Loads the configuration. - - - - - Dynamic scheme manager which not persist the changes. - - The type of the scheme definition. - - - - Gets the type of the managed handler. - - - The type of the managed handler. - - - - - Initializes a new instance of the class. - - The scheme provider. - The wrapper factory. - The list of managed handlers types. - - schemeProvider - or - wrapperFactory - or - store - - - - - Adds a scheme asynchronously. - - The definition. - The cancellation token. - - definition - - - - Updates the scheme asynchronous. - - The definition. - The cancellation token. - - definition - The scheme does not exist. - - - - Removes the scheme asynchronous. - - The scheme. - The cancellation token. - - scheme cannot be null or white space. - - - - serializer interface - - - - - Deserializes the type. - - The value. - - - - - Serializes the type. - - The type. - - - - - Deserializes the specified value. - - The value. - Type of the options. - An AuthenticationSchemeOptions instance. - - - - Serializes the specified options. - - The options. - Type of the options. - The serialized result. - - - - Interface for store use by - - The type of the scheme definition. - - - - Gets the scheme definitions list. - - - The scheme definitions list. - - - - - Adds a defnition asynchronously. - - The definition. - The cancellation token. - - - - - Finds scheme definition by scheme asynchronous. - - The scheme. - The cancellation token. - An instance of TSchemeDefinition or null. - - - - Removes a scheme definition asynchronous. - - The definition. - The cancellation token. - - - - - Updates a scheme definition asynchronous. - - The definition. - The cancellation token. - - - - - Wrapper for - - For internal use, you should not use this class - The type of the options. - - - - - Initializes a new instance of the class. - - The parent. - The post configures actions list. - The on added action. - - parent - or - postConfigures - or - onAdded - - For internal user, you should not use this class - - - - Clears all options instances from the cache. - - - - - Gets a named options instance, or adds a new instance created with createOptions. - - The name of the options instance. - The func used to create the new instance. - - The options instance. - - - This method is not implemented. - - - - Tries to adds a new option to the cache, will return false if the name already exists. - - The name of the options instance. - The options instance. - - Whether anything was added. - - - - - Try to remove an options instance. - - The name of the options instance. - - Whether anything was removed. - - - - - Factory to create wrapper for - - For internal user, you should not use this class - - - - Initializes a new instance of the class. - - The service provider. - For internal user, you should not use this class - - - - Gets the wrapper for the option type - - Type of the options. - - For internal user, you should not use this class - - - - Base class for scheme definition - - - - - Gets or sets the scheme. - - - The scheme. - - - - - Gets or sets the display name. - - - The display name. - - - - - Gets or sets the type of the handler. - - - The type of the handler. - - - - - Gets or sets the options. - - - The options. - - - - - Type extensions - - - - - Gets the type of the authentication scheme options. - - Type of the handler. - - Parameter handlerType should be a } - - - diff --git a/src/Aguacongas.AspNetCore.Authentication/DynamicAuthenticationBuilder.cs b/src/Aguacongas.AspNetCore.Authentication/DynamicAuthenticationBuilder.cs index 31bbf6d..bfb79a1 100644 --- a/src/Aguacongas.AspNetCore.Authentication/DynamicAuthenticationBuilder.cs +++ b/src/Aguacongas.AspNetCore.Authentication/DynamicAuthenticationBuilder.cs @@ -1,7 +1,6 @@ // Project: aguacongas/DymamicAuthProviders // Copyright (c) 2018 @Olivier Lefebvre using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; @@ -92,13 +91,11 @@ public override AuthenticationBuilder AddScheme(string authe private class EnsureUniqCallbackPath : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions { - private readonly AuthenticationOptions _authOptions; private readonly IAuthenticationSchemeProvider _schemeProvider; private readonly IOptionsMonitorCache _monitorCache; - public EnsureUniqCallbackPath(IOptions authOptions, IAuthenticationSchemeProvider schemeProvider, IOptionsMonitorCache monitorCache) + public EnsureUniqCallbackPath(IAuthenticationSchemeProvider schemeProvider, IOptionsMonitorCache monitorCache) { - _authOptions = authOptions.Value; _schemeProvider = schemeProvider; _monitorCache = monitorCache; } @@ -108,8 +105,12 @@ public void PostConfigure(string name, TOptions options) var schemes = _schemeProvider.GetAllSchemesAsync().GetAwaiter().GetResult(); foreach(var scheme in schemes) { + if (name == scheme.Name) + { + continue; + } var other = _monitorCache.GetOrAdd(scheme.Name, () => options); - if (other != options && other is RemoteAuthenticationOptions otherRemote && otherRemote.CallbackPath == options.CallbackPath) + if (other is RemoteAuthenticationOptions otherRemote && otherRemote.CallbackPath == options.CallbackPath) { throw new InvalidOperationException($"Callbacks paths for schemes {name} and {scheme.Name} are equals: {options.CallbackPath}"); } diff --git a/src/Aguacongas.AspNetCore.Authentication/DynamicManager.cs b/src/Aguacongas.AspNetCore.Authentication/DynamicManager.cs index db20b9c..933a291 100644 --- a/src/Aguacongas.AspNetCore.Authentication/DynamicManager.cs +++ b/src/Aguacongas.AspNetCore.Authentication/DynamicManager.cs @@ -108,7 +108,10 @@ public virtual void Load() foreach (var definition in _store.SchemeDefinitions) { - base.AddAsync(definition).GetAwaiter().GetResult(); + if (ManagedHandlerType.Contains(definition.HandlerType)) + { + base.AddAsync(definition).GetAwaiter().GetResult(); + } } } } diff --git a/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicManagerTest.cs b/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicManagerTest.cs index 033c747..139f7f8 100644 --- a/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicManagerTest.cs +++ b/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicManagerTest.cs @@ -10,6 +10,7 @@ namespace Aguacongas.AspNetCore.Authentication.EntityFramework.Test { public class DynamicManagerTest: DynamicManagerTestBase { + private readonly string dbName = Guid.NewGuid().ToString(); public DynamicManagerTest(ITestOutputHelper output): base(output) { } @@ -18,7 +19,7 @@ protected override DynamicAuthenticationBuilder AddStore(DynamicAuthenticationBu { builder.Services.AddDbContext(options => { - options.UseInMemoryDatabase(Guid.NewGuid().ToString()); + options.UseInMemoryDatabase(dbName); }); return builder.AddEntityFrameworkStore(); } diff --git a/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicProviderStoreTest.cs b/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicProviderStoreTest.cs index 78dca54..82d9bec 100644 --- a/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicProviderStoreTest.cs +++ b/test/Aguacongas.AspNetCore.Authentication.EntityFramework.Test/DynamicProviderStoreTest.cs @@ -1,8 +1,11 @@ // Project: aguacongas/DymamicAuthProviders // Copyright (c) 2018 @Olivier Lefebvre using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Moq; using System; @@ -33,7 +36,7 @@ public async Task Assertions() } } - public class FakeDbContextOptions : DbContextOptions + class FakeDbContextOptions : DbContextOptions { public FakeDbContextOptions() : base(new Mock>().Object) { diff --git a/test/Aguacongas.AspNetCore.Authentication.Redis.Test/Aguacongas.AspNetCore.Authentication.Redis.Test.csproj b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/Aguacongas.AspNetCore.Authentication.Redis.Test.csproj new file mode 100644 index 0000000..d70261b --- /dev/null +++ b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/Aguacongas.AspNetCore.Authentication.Redis.Test.csproj @@ -0,0 +1,25 @@ + + + + netcoreapp2.2 + + false + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + diff --git a/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicManagerTest.cs b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicManagerTest.cs new file mode 100644 index 0000000..27ed744 --- /dev/null +++ b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicManagerTest.cs @@ -0,0 +1,24 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre +using Aguacongas.AspNetCore.Authentication.TestBase; +using Microsoft.Extensions.DependencyInjection; +using Xunit; +using Xunit.Abstractions; + +namespace Aguacongas.AspNetCore.Authentication.Redis.Test +{ + [Collection("Redis")] + public class DynamicManagerTest: DynamicManagerTestBase + { + private readonly TestFixture _fixture; + public DynamicManagerTest(ITestOutputHelper output, TestFixture fixture): base(output) + { + _fixture = fixture; + } + + protected override DynamicAuthenticationBuilder AddStore(DynamicAuthenticationBuilder builder) + { + return builder.AddRedisStore(provider => _fixture.Database); + } + } +} diff --git a/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicProviderStoreTest.cs b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicProviderStoreTest.cs new file mode 100644 index 0000000..fd8dac6 --- /dev/null +++ b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/DynamicProviderStoreTest.cs @@ -0,0 +1,75 @@ +// Project: aguacongas/DymamicAuthProviders +// Copyright (c) 2018 @Olivier Lefebvre +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.Logging; +using Moq; +using StackExchange.Redis; +using System; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace Aguacongas.AspNetCore.Authentication.Redis.Test +{ + [Collection("Redis")] + public class DynamicProviderStoreTest + { + private readonly ITestOutputHelper _output; + private readonly TestFixture _fixture; + + public DynamicProviderStoreTest(ITestOutputHelper output, TestFixture fixture) + { + _output = output; + _fixture = fixture; + } + + [Fact] + public async Task Assertions() + { + Assert.Throws(() => new DynamicProviderStore(null, null, null)); + var databaseMock = new Mock().Object; + Assert.Throws(() => new DynamicProviderStore(databaseMock, null, null)); + Assert.Throws(() => new DynamicProviderStore(databaseMock, new RedisAuthenticationSchemeOptionsSerializer(), null)); + var loggerMock = new Mock>>().Object; + var store = new DynamicProviderStore(databaseMock, new RedisAuthenticationSchemeOptionsSerializer(), loggerMock); + await Assert.ThrowsAsync(() => store.AddAsync(null)); + await Assert.ThrowsAsync(() => store.UpdateAsync(null)); + await Assert.ThrowsAsync(() => store.RemoveAsync(null)); + await Assert.ThrowsAsync(() => store.FindBySchemeAsync(null)); + await Assert.ThrowsAsync(() => store.FindBySchemeAsync("")); + await Assert.ThrowsAsync(() => store.FindBySchemeAsync(" ")); + } + + [Fact] + public async Task Concurrent_updates_should_fail() + { + var db = CreateDabase(); + var store = CreateStore(db); + + var scheme = Guid.NewGuid().ToString(); + await store.AddAsync(new SchemeDefinition + { + Scheme = scheme, + DisplayName = "test", + HandlerType = typeof(CookieAuthenticationHandler), + Options = new CookieAuthenticationOptions() + }); + + var definition = await store.FindBySchemeAsync(scheme); + var definition2 = await store.FindBySchemeAsync(scheme); + + await store.UpdateAsync(definition); + + await Assert.ThrowsAsync(() => store.UpdateAsync(definition2)); + } + + public DynamicProviderStore CreateStore(IDatabase database) + { + return new DynamicProviderStore(database, new RedisAuthenticationSchemeOptionsSerializer(), new Mock>().Object); + } + public IDatabase CreateDabase() + { + return _fixture.Database; + } + } +} diff --git a/test/Aguacongas.AspNetCore.Authentication.Redis.Test/TestFixture.cs b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/TestFixture.cs new file mode 100644 index 0000000..801de5f --- /dev/null +++ b/test/Aguacongas.AspNetCore.Authentication.Redis.Test/TestFixture.cs @@ -0,0 +1,27 @@ +using StackExchange.Redis; +using Xunit; + +namespace Aguacongas.AspNetCore.Authentication.Redis.Test +{ + public class TestFixture + { + public IDatabase Database { get; } + + public TestFixture() + { + var options = new ConfigurationOptions(); + options.EndPoints.Add("localhost:6379"); + options.AllowAdmin = true; + + var multiplexer = ConnectionMultiplexer.Connect(options); + Database = multiplexer.GetDatabase(); + var server = multiplexer.GetServer("localhost:6379"); + server.FlushDatabase(); + } + } + + [CollectionDefinition("Redis")] + public class CollectionFixture: ICollectionFixture + { + } +}