From 32fe4b0ae7b13e4eb57839e24b5d94939487941d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 15 May 2018 20:10:47 +0200 Subject: [PATCH] Remove OpenIddict.Models/OpenIddict.Stores and move to per-provider models --- OpenIddict.sln | 27 +- .../Controllers/AuthorizationController.cs | 2 +- samples/Mvc.Server/Startup.cs | 10 +- .../Managers/IOpenIddictApplicationManager.cs | 2 +- .../IOpenIddictAuthorizationManager.cs | 2 +- .../Managers/IOpenIddictScopeManager.cs | 2 +- .../Managers/IOpenIddictTokenManager.cs | 2 +- .../OpenIddictConstants.cs | 5 + .../OpenIddictException.cs | 75 ++ .../Managers/OpenIddictApplicationManager.cs | 2 +- .../OpenIddictAuthorizationManager.cs | 2 +- .../Managers/OpenIddictScopeManager.cs | 2 +- .../Managers/OpenIddictTokenManager.cs | 2 +- src/OpenIddict.Core/OpenIddictCoreBuilder.cs | 234 +++-- src/OpenIddict.Core/OpenIddictCoreOptions.cs | 3 + .../OpenIddict.EntityFramework.Models.csproj} | 2 +- .../OpenIddictApplication.cs | 105 +++ .../OpenIddictAuthorization.cs | 77 ++ .../OpenIddictScope.cs | 2 +- .../OpenIddictToken.cs | 97 ++ .../OpenIddict.EntityFramework.csproj | 2 +- .../OpenIddictEntityFrameworkBuilder.cs | 111 +++ ...=> OpenIddictEntityFrameworkExtensions.cs} | 58 +- .../OpenIddictEntityFrameworkOptions.cs | 25 + .../OpenIddictApplicationStoreResolver.cs | 39 +- .../OpenIddictAuthorizationStoreResolver.cs | 39 +- .../Resolvers/OpenIddictScopeStoreResolver.cs | 39 +- .../Resolvers/OpenIddictTokenStoreResolver.cs | 39 +- .../Stores/OpenIddictApplicationStore.cs | 793 ++++++++++++++++- .../Stores/OpenIddictAuthorizationStore.cs | 623 +++++++++++-- .../Stores/OpenIddictScopeStore.cs | 534 ++++++++++- .../Stores/OpenIddictTokenStore.cs | 698 +++++++++++++-- ...enIddict.EntityFrameworkCore.Models.csproj | 15 + .../OpenIddictApplication.cs | 2 +- .../OpenIddictAuthorization.cs | 5 +- .../OpenIddictScope.cs | 69 ++ .../OpenIddictToken.cs | 2 +- .../OpenIddict.EntityFrameworkCore.csproj | 2 +- .../OpenIddictEntityFrameworkCoreBuilder.cs | 123 +++ ...penIddictEntityFrameworkCoreCustomizer.cs} | 6 +- ...penIddictEntityFrameworkCoreExtensions.cs} | 61 +- .../OpenIddictEntityFrameworkCoreOptions.cs | 25 + .../OpenIddictApplicationStoreResolver.cs | 39 +- .../OpenIddictAuthorizationStoreResolver.cs | 38 +- .../Resolvers/OpenIddictScopeStoreResolver.cs | 39 +- .../Resolvers/OpenIddictTokenStoreResolver.cs | 39 +- .../Stores/OpenIddictApplicationStore.cs | 649 +++++++++++++- .../Stores/OpenIddictAuthorizationStore.cs | 447 ++++++++-- .../Stores/OpenIddictScopeStore.cs | 420 ++++++++- .../Stores/OpenIddictTokenStore.cs | 578 ++++++++++-- .../OpenIddictServerProvider.Helpers.cs | 44 +- .../OpenIddictServerOptions.cs | 10 +- .../OpenIddict.Stores.csproj | 25 - .../Stores/OpenIddictApplicationStore.cs | 832 ------------------ .../Stores/OpenIddictAuthorizationStore.cs | 694 --------------- .../Stores/OpenIddictScopeStore.cs | 577 ------------ .../Stores/OpenIddictTokenStore.cs | 805 ----------------- src/OpenIddict/OpenIddict.csproj | 5 - src/OpenIddict/OpenIddictExtensions.cs | 57 -- .../OpenIddictCoreBuilderTests.cs | 10 +- ...penIddictEntityFrameworkExtensionsTests.cs | 57 ++ .../OpenIddictExtensionsTests.cs | 35 - ...dictEntityFrameworkCoreExtensionsTests.cs} | 32 +- .../OpenIddictExtensionsTests.cs | 34 - .../OpenIddictServerInitializerTests.cs | 14 +- ...ddictServerProviderTests.Authentication.cs | 1 - .../OpenIddictServerProviderTests.Exchange.cs | 1 - ...IddictServerProviderTests.Introspection.cs | 1 - ...penIddictServerProviderTests.Revocation.cs | 1 - ...IddictServerProviderTests.Serialization.cs | 76 +- .../Internal/OpenIddictServerProviderTests.cs | 11 +- .../OpenIddictServerBuilderTests.cs | 14 +- test/OpenIddict.Tests/OpenIddict.Tests.csproj | 27 - .../OpenIddictValidationHandlerTests.cs | 11 +- 74 files changed, 5806 insertions(+), 3782 deletions(-) create mode 100644 src/OpenIddict.Abstractions/OpenIddictException.cs rename src/{OpenIddict.Models/OpenIddict.Models.csproj => OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj} (73%) create mode 100644 src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs create mode 100644 src/OpenIddict.EntityFramework.Models/OpenIddictAuthorization.cs rename src/{OpenIddict.Models => OpenIddict.EntityFramework.Models}/OpenIddictScope.cs (97%) create mode 100644 src/OpenIddict.EntityFramework.Models/OpenIddictToken.cs create mode 100644 src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs rename src/OpenIddict.EntityFramework/{OpenIddictExtensions.cs => OpenIddictEntityFrameworkExtensions.cs} (79%) create mode 100644 src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkOptions.cs create mode 100644 src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj rename src/{OpenIddict.Models => OpenIddict.EntityFrameworkCore.Models}/OpenIddictApplication.cs (98%) rename src/{OpenIddict.Models => OpenIddict.EntityFrameworkCore.Models}/OpenIddictAuthorization.cs (95%) create mode 100644 src/OpenIddict.EntityFrameworkCore.Models/OpenIddictScope.cs rename src/{OpenIddict.Models => OpenIddict.EntityFrameworkCore.Models}/OpenIddictToken.cs (98%) create mode 100644 src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs rename src/OpenIddict.EntityFrameworkCore/{OpenIddictCustomizer.cs => OpenIddictEntityFrameworkCoreCustomizer.cs} (84%) rename src/OpenIddict.EntityFrameworkCore/{OpenIddictExtensions.cs => OpenIddictEntityFrameworkCoreExtensions.cs} (80%) create mode 100644 src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs delete mode 100644 src/OpenIddict.Stores/OpenIddict.Stores.csproj delete mode 100644 src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs delete mode 100644 src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs delete mode 100644 src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs delete mode 100644 src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs delete mode 100644 src/OpenIddict/OpenIddictExtensions.cs create mode 100644 test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs delete mode 100644 test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs rename test/{OpenIddict.Tests/OpenIddictExtensionsTests.cs => OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs} (62%) delete mode 100644 test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs delete mode 100644 test/OpenIddict.Tests/OpenIddict.Tests.csproj diff --git a/OpenIddict.sln b/OpenIddict.sln index 677af767..98a99180 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -38,25 +38,23 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkC EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Mvc.Tests", "test\OpenIddict.Mvc.Tests\OpenIddict.Mvc.Tests.csproj", "{8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Tests", "test\OpenIddict.Tests\OpenIddict.Tests.csproj", "{3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Models", "src\OpenIddict.Models\OpenIddict.Models.csproj", "{0102A6CC-41A6-4B34-B49E-65AFE95882BB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework.Models", "src\OpenIddict.EntityFramework.Models\OpenIddict.EntityFramework.Models.csproj", "{0102A6CC-41A6-4B34-B49E-65AFE95882BB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework", "src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj", "{BF42CC6C-0B56-4F66-9866-18B8393F3C06}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework.Tests", "test\OpenIddict.EntityFramework.Tests\OpenIddict.EntityFramework.Tests.csproj", "{96325E37-9897-43AC-8408-7B17F58E8788}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Stores", "src\OpenIddict.Stores\OpenIddict.Stores.csproj", "{275D888A-B4C8-4E93-AC4B-B1AA25D98159}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server", "src\OpenIddict.Server\OpenIddict.Server.csproj", "{21A7F241-CBE7-4F5C-9787-F2C50D135AEA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", "test\OpenIddict.Server.Tests\OpenIddict.Server.Tests.csproj", "{07B02B98-8A68-432D-A932-48E6D52B221A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Abstractions", "src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj", "{886A16DA-C9CF-4979-9B38-D06DF8A714B6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Abstractions", "src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj", "{886A16DA-C9CF-4979-9B38-D06DF8A714B6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{6AB8F9E7-47F8-4A40-837F-C8753362AF54}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Validation", "src\OpenIddict.Validation\OpenIddict.Validation.csproj", "{6AB8F9E7-47F8-4A40-837F-C8753362AF54}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkCore.Models", "src\OpenIddict.EntityFrameworkCore.Models\OpenIddict.EntityFrameworkCore.Models.csproj", "{B5371534-4C33-41FA-B3D3-7D70D632DB15}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -100,10 +98,6 @@ Global {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Release|Any CPU.Build.0 = Release|Any CPU - {3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}.Release|Any CPU.Build.0 = Release|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -116,10 +110,6 @@ Global {96325E37-9897-43AC-8408-7B17F58E8788}.Debug|Any CPU.Build.0 = Debug|Any CPU {96325E37-9897-43AC-8408-7B17F58E8788}.Release|Any CPU.ActiveCfg = Release|Any CPU {96325E37-9897-43AC-8408-7B17F58E8788}.Release|Any CPU.Build.0 = Release|Any CPU - {275D888A-B4C8-4E93-AC4B-B1AA25D98159}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {275D888A-B4C8-4E93-AC4B-B1AA25D98159}.Debug|Any CPU.Build.0 = Debug|Any CPU - {275D888A-B4C8-4E93-AC4B-B1AA25D98159}.Release|Any CPU.ActiveCfg = Release|Any CPU - {275D888A-B4C8-4E93-AC4B-B1AA25D98159}.Release|Any CPU.Build.0 = Release|Any CPU {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU {21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -140,6 +130,10 @@ Global {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Debug|Any CPU.Build.0 = Debug|Any CPU {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.ActiveCfg = Release|Any CPU {6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.Build.0 = Release|Any CPU + {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5371534-4C33-41FA-B3D3-7D70D632DB15}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -154,16 +148,15 @@ Global {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {7831F17A-DF0B-42EC-841B-065A9B5BD786} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} - {3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {0102A6CC-41A6-4B34-B49E-65AFE95882BB} = {D544447C-D701-46BB-9A5B-C76C612A596B} {BF42CC6C-0B56-4F66-9866-18B8393F3C06} = {D544447C-D701-46BB-9A5B-C76C612A596B} {96325E37-9897-43AC-8408-7B17F58E8788} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} - {275D888A-B4C8-4E93-AC4B-B1AA25D98159} = {D544447C-D701-46BB-9A5B-C76C612A596B} {21A7F241-CBE7-4F5C-9787-F2C50D135AEA} = {D544447C-D701-46BB-9A5B-C76C612A596B} {07B02B98-8A68-432D-A932-48E6D52B221A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {886A16DA-C9CF-4979-9B38-D06DF8A714B6} = {D544447C-D701-46BB-9A5B-C76C612A596B} {F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B} + {B5371534-4C33-41FA-B3D3-7D70D632DB15} = {D544447C-D701-46BB-9A5B-C76C612A596B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index a01a54b2..669b07fb 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -20,7 +20,7 @@ using Mvc.Server.ViewModels.Authorization; using Mvc.Server.ViewModels.Shared; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; using OpenIddict.Server; namespace Mvc.Server diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index ea773833..9624c63d 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -10,7 +10,7 @@ using Mvc.Server.Models; using Mvc.Server.Services; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace Mvc.Server { @@ -71,11 +71,9 @@ namespace Mvc.Server // Register the OpenIddict core services. .AddCore(options => { - // Configure OpenIddict to use the default models. - options.UseDefaultModels(); - - // Register the Entity Framework stores. - options.AddEntityFrameworkCoreStores(); + // Configure OpenIddict to use the Entity Framework Core stores and models. + options.UseEntityFrameworkCore() + .UseDbContext(); }) // Register the OpenIddict server handler. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs index 26b2aa87..717fa176 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs @@ -287,7 +287,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); + Task> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 5a848f6b..20864380 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -306,7 +306,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); + Task> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs index 08d21856..fee44974 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs @@ -205,7 +205,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); + Task> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index d70cbad3..a6d0032c 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -309,7 +309,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); + Task> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 1d6e481b..c1dd31ba 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -39,6 +39,11 @@ namespace OpenIddict.Abstractions public const string LogoutRequest = "openiddict-logout-request:"; } + public static class Exceptions + { + public const string ConcurrencyError = "concurrency_error"; + } + public static class Permissions { public static class Endpoints diff --git a/src/OpenIddict.Abstractions/OpenIddictException.cs b/src/OpenIddict.Abstractions/OpenIddictException.cs new file mode 100644 index 00000000..003bf397 --- /dev/null +++ b/src/OpenIddict.Abstractions/OpenIddictException.cs @@ -0,0 +1,75 @@ +using System; +using System.Runtime.Serialization; + +namespace OpenIddict.Abstractions +{ + /// + /// Represents an OpenIddict exception. + /// + public class OpenIddictException : Exception + { + /// + /// Creates a new . + /// + /// The reason of the exception. + /// The exception message. + public OpenIddictException(string reason, string message) + : base(message) + { + Reason = reason; + } + + /// + /// Creates a new . + /// + /// The reason of the exception. + /// The exception message. + /// The inner exception. + public OpenIddictException(string reason, string message, Exception innerException) + : base(message, innerException) + { + Reason = reason; + } + + /// + /// Creates a new . + /// + /// + /// The that holds the serialized object data about the exception being thrown. + /// + /// + /// The that contains contextual information about the source or destination. + /// + protected OpenIddictException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + Reason = info.GetString(nameof(Reason)); + } + + /// + /// Gets the reason that caused the exception to be thrown. + /// + public string Reason { get; } + + /// + /// Serializes the members of this class. + /// + /// + /// The that holds the serialized object data about the exception being thrown. + /// + /// + /// The that contains contextual information about the source or destination. + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException(nameof(info)); + } + + info.AddValue(nameof(Reason), Reason); + + base.GetObjectData(info, context); + } + } +} diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 0fedc51e..67e7238b 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -764,7 +764,7 @@ namespace OpenIddict.Core /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( - [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default) + [CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default) { return Store.ListAsync(count, offset, cancellationToken); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 199bfc96..293acf93 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -756,7 +756,7 @@ namespace OpenIddict.Core /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( - [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default) + [CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default) { return Store.ListAsync(count, offset, cancellationToken); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index 03504067..80849fd0 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -433,7 +433,7 @@ namespace OpenIddict.Core /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( - [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default) + [CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default) { return Store.ListAsync(count, offset, cancellationToken); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 3787aa03..69eec87f 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -622,7 +622,7 @@ namespace OpenIddict.Core /// whose result returns all the elements returned when executing the specified query. /// public virtual Task> ListAsync( - [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default) + [CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default) { return Store.ListAsync(count, offset, cancellationToken); } diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index 449063ea..59a75f57 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -63,9 +63,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddApplicationStore() where TStore : class - => AddApplicationStore(typeof(TStore)); + public OpenIddictCoreBuilder AddApplicationStore(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TStore : class + => AddApplicationStore(typeof(TStore), lifetime); /// /// Adds a custom application store by a custom implementation derived @@ -74,8 +76,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddApplicationStore([NotNull] Type type) + public OpenIddictCoreBuilder AddApplicationStore( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -97,13 +101,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictApplicationStore<>), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictApplicationStore<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictApplicationStore<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictApplicationStore<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -116,9 +120,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddAuthorizationStore() where TStore : class - => AddAuthorizationStore(typeof(TStore)); + public OpenIddictCoreBuilder AddAuthorizationStore(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TStore : class + => AddAuthorizationStore(typeof(TStore), lifetime); /// /// Adds a custom authorization store by a custom implementation derived @@ -127,8 +133,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddAuthorizationStore([NotNull] Type type) + public OpenIddictCoreBuilder AddAuthorizationStore( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -150,13 +158,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictAuthorizationStore<>), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictAuthorizationStore<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictAuthorizationStore<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictAuthorizationStore<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -169,9 +177,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddScopeStore() where TStore : class - => AddScopeStore(typeof(TStore)); + public OpenIddictCoreBuilder AddScopeStore(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TStore : class + => AddScopeStore(typeof(TStore), lifetime); /// /// Adds a custom scope store by a custom implementation derived @@ -180,8 +190,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddScopeStore([NotNull] Type type) + public OpenIddictCoreBuilder AddScopeStore( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -203,13 +215,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictScopeStore<>), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictScopeStore<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictScopeStore<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictScopeStore<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -222,9 +234,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddTokenStore() where TStore : class - => AddTokenStore(typeof(TStore)); + public OpenIddictCoreBuilder AddTokenStore(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TStore : class + => AddTokenStore(typeof(TStore), lifetime); /// /// Adds a custom token store by a custom implementation derived @@ -233,8 +247,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder AddTokenStore([NotNull] Type type) + public OpenIddictCoreBuilder AddTokenStore( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -256,18 +272,31 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictTokenStore<>), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictTokenStore<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictTokenStore<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictTokenStore<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; } + /// + /// Replace the default application manager by a custom manager derived + /// from . + /// Note: when using this overload, the application manager + /// must be either a non-generic or closed generic service. + /// + /// The type of the custom manager. + /// The lifetime of the registered service. + /// The . + public OpenIddictCoreBuilder ReplaceApplicationManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TManager : class + => ReplaceApplicationManager(typeof(TManager), lifetime); + /// /// Replace the default application manager by a custom manager derived /// from . @@ -275,8 +304,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceApplicationManager([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceApplicationManager( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -298,13 +329,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -314,17 +345,20 @@ namespace Microsoft.Extensions.DependencyInjection /// Replaces the default application store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceApplicationStoreResolver() + public OpenIddictCoreBuilder ReplaceApplicationStoreResolver(ServiceLifetime lifetime = ServiceLifetime.Scoped) where TResolver : IOpenIddictApplicationStoreResolver - => ReplaceApplicationStoreResolver(typeof(TResolver)); + => ReplaceApplicationStoreResolver(typeof(TResolver), lifetime); /// /// Replaces the default application store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceApplicationStoreResolver([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceApplicationStoreResolver( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -336,22 +370,11 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictApplicationStoreResolver), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictApplicationStoreResolver), type, lifetime)); return this; } - /// - /// Replace the default application manager by a custom manager derived - /// from . - /// Note: when using this overload, the application manager - /// must be either a non-generic or closed generic service. - /// - /// The type of the custom manager. - /// The . - public OpenIddictCoreBuilder ReplaceApplicationManager() where TManager : class - => ReplaceApplicationManager(typeof(TManager)); - /// /// Replace the default authorization manager by a custom manager derived /// from . @@ -359,9 +382,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationManager() where TManager : class - => ReplaceAuthorizationManager(typeof(TManager)); + public OpenIddictCoreBuilder ReplaceAuthorizationManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TManager : class + => ReplaceAuthorizationManager(typeof(TManager), lifetime); /// /// Replace the default authorization manager by a custom manager derived @@ -370,8 +395,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationManager([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceAuthorizationManager( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -393,13 +420,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -409,17 +436,20 @@ namespace Microsoft.Extensions.DependencyInjection /// Replaces the default authorization store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver() + public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver(ServiceLifetime lifetime = ServiceLifetime.Scoped) where TResolver : IOpenIddictAuthorizationStoreResolver - => ReplaceAuthorizationStoreResolver(typeof(TResolver)); + => ReplaceAuthorizationStoreResolver(typeof(TResolver), lifetime); /// /// Replaces the default authorization store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -431,7 +461,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictAuthorizationStoreResolver), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictAuthorizationStoreResolver), type, lifetime)); return this; } @@ -444,8 +474,9 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The type of the custom manager. /// The . - public OpenIddictCoreBuilder ReplaceScopeManager() where TManager : class - => ReplaceScopeManager(typeof(TManager)); + public OpenIddictCoreBuilder ReplaceScopeManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TManager : class + => ReplaceScopeManager(typeof(TManager), lifetime); /// /// Replace the default scope manager by a custom manager @@ -454,8 +485,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceScopeManager([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceScopeManager( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -477,13 +510,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -493,17 +526,20 @@ namespace Microsoft.Extensions.DependencyInjection /// Replaces the default scope store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceScopeStoreResolver() + public OpenIddictCoreBuilder ReplaceScopeStoreResolver(ServiceLifetime lifetime = ServiceLifetime.Scoped) where TResolver : IOpenIddictScopeStoreResolver - => ReplaceScopeStoreResolver(typeof(TResolver)); + => ReplaceScopeStoreResolver(typeof(TResolver), lifetime); /// /// Replaces the default scope store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceScopeStoreResolver([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceScopeStoreResolver( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -515,7 +551,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictScopeStoreResolver), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictScopeStoreResolver), type, lifetime)); return this; } @@ -527,9 +563,11 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenManager() where TManager : class - => ReplaceTokenManager(typeof(TManager)); + public OpenIddictCoreBuilder ReplaceTokenManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TManager : class + => ReplaceTokenManager(typeof(TManager), lifetime); /// /// Replace the default token manager by a custom manager @@ -538,8 +576,10 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenManager([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceTokenManager( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -561,13 +601,13 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>), type, lifetime)); } else { - Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type)); + Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); } return this; @@ -577,17 +617,20 @@ namespace Microsoft.Extensions.DependencyInjection /// Replaces the default token store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenStoreResolver() + public OpenIddictCoreBuilder ReplaceTokenStoreResolver(ServiceLifetime lifetime = ServiceLifetime.Scoped) where TResolver : IOpenIddictTokenStoreResolver - => ReplaceTokenStoreResolver(typeof(TResolver)); + => ReplaceTokenStoreResolver(typeof(TResolver), lifetime); /// /// Replaces the default token store resolver by a custom implementation. /// /// The type of the custom store. + /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenStoreResolver([NotNull] Type type) + public OpenIddictCoreBuilder ReplaceTokenStoreResolver( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) { if (type == null) { @@ -599,30 +642,41 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictTokenStoreResolver), type)); + Services.Replace(new ServiceDescriptor(typeof(IOpenIddictTokenStoreResolver), type, lifetime)); return this; } /// - /// Configures OpenIddict to use the specified entities. + /// Configures OpenIddict to use the specified entity as the default application entity. + /// + /// The . + public OpenIddictCoreBuilder SetDefaultApplicationEntity() + where TApplication : class, new() + => Configure(options => options.DefaultApplicationType = typeof(TApplication)); + + /// + /// Configures OpenIddict to use the specified entity as the default authorization entity. + /// + /// The . + public OpenIddictCoreBuilder SetDefaultAuthorizationEntity() + where TAuthorization : class, new() + => Configure(options => options.DefaultAuthorizationType = typeof(TAuthorization)); + + /// + /// Configures OpenIddict to use the specified entity as the default scope entity. + /// + /// The . + public OpenIddictCoreBuilder SetDefaultScopeEntity() + where TScope : class, new() + => Configure(options => options.DefaultScopeType = typeof(TScope)); + + /// + /// Configures OpenIddict to use the specified entity as the default token entity. /// - /// The type corresponding to the Application entity. - /// The type corresponding to the Authorization entity. - /// The type corresponding to the Scope entity. - /// The type corresponding to the Token entity. /// The . - public OpenIddictCoreBuilder UseCustomModels() - where TApplication : class - where TAuthorization : class - where TScope : class - where TToken : class - => Configure(options => - { - options.DefaultApplicationType = typeof(TApplication); - options.DefaultAuthorizationType = typeof(TAuthorization); - options.DefaultScopeType = typeof(TScope); - options.DefaultTokenType = typeof(TToken); - }); + public OpenIddictCoreBuilder SetDefaultTokenEntity() + where TToken : class, new() + => Configure(options => options.DefaultTokenType = typeof(TToken)); } } \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictCoreOptions.cs b/src/OpenIddict.Core/OpenIddictCoreOptions.cs index f805190c..eef1208e 100644 --- a/src/OpenIddict.Core/OpenIddictCoreOptions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreOptions.cs @@ -8,6 +8,9 @@ using System; namespace OpenIddict.Core { + /// + /// Provides various settings needed to configure the OpenIddict core services. + /// public class OpenIddictCoreOptions { /// diff --git a/src/OpenIddict.Models/OpenIddict.Models.csproj b/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj similarity index 73% rename from src/OpenIddict.Models/OpenIddict.Models.csproj rename to src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj index dad3203d..6deabadb 100644 --- a/src/OpenIddict.Models/OpenIddict.Models.csproj +++ b/src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj @@ -7,7 +7,7 @@ - OpenIddict's default entities, used by the Entity Framework 6.x and Entity Framework Core stores. + Relational entities for the Entity Framework 6.x stores. Kévin Chalet aspnetcore;authentication;jwt;openidconnect;openiddict;security diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs new file mode 100644 index 00000000..2134d879 --- /dev/null +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs @@ -0,0 +1,105 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; + +namespace OpenIddict.EntityFramework.Models +{ + /// + /// Represents an OpenIddict application. + /// + public class OpenIddictApplication : OpenIddictApplication + { + public OpenIddictApplication() + { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict application. + /// + public class OpenIddictApplication where TKey : IEquatable + { + /// + /// Gets the list of the authorizations associated with this application. + /// + public virtual IList Authorizations { get; } = new List(); + + /// + /// Gets or sets the client identifier + /// associated with the current application. + /// + public virtual string ClientId { get; set; } + + /// + /// Gets or sets the client secret associated with the current application. + /// Note: depending on the application manager used to create this instance, + /// this property may be hashed or encrypted for security reasons. + /// + public virtual string ClientSecret { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the consent type + /// associated with the current application. + /// + public virtual string ConsentType { get; set; } + + /// + /// Gets or sets the display name + /// associated with the current application. + /// + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current application. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the permissions associated with the + /// current application, serialized as a JSON array. + /// + public virtual string Permissions { get; set; } + + /// + /// Gets or sets the logout callback URLs associated with + /// the current application, serialized as a JSON array. + /// + public virtual string PostLogoutRedirectUris { get; set; } + + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current application. + /// + public virtual string Properties { get; set; } + + /// + /// Gets or sets the callback URLs associated with the + /// current application, serialized as a JSON array. + /// + public virtual string RedirectUris { get; set; } + + /// + /// Gets the list of the tokens associated with this application. + /// + public virtual IList Tokens { get; } = new List(); + + /// + /// Gets or sets the application type + /// associated with the current application. + /// + public virtual string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictAuthorization.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictAuthorization.cs new file mode 100644 index 00000000..2343329a --- /dev/null +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictAuthorization.cs @@ -0,0 +1,77 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; + +namespace OpenIddict.EntityFramework.Models +{ + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization : OpenIddictAuthorization + { + public OpenIddictAuthorization() + { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization where TKey : IEquatable + { + /// + /// Gets or sets the application associated with the current authorization. + /// + public virtual TApplication Application { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the unique identifier + /// associated with the current authorization. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current authorization. + /// + public virtual string Properties { get; set; } + + /// + /// Gets or sets the scopes associated with the current + /// authorization, serialized as a JSON array. + /// + public virtual string Scopes { get; set; } + + /// + /// Gets or sets the status of the current authorization. + /// + public virtual string Status { get; set; } + + /// + /// Gets or sets the subject associated with the current authorization. + /// + public virtual string Subject { get; set; } + + /// + /// Gets the list of tokens associated with the current authorization. + /// + public virtual IList Tokens { get; } = new List(); + + /// + /// Gets or sets the type of the current authorization. + /// + public virtual string Type { get; set; } + } +} diff --git a/src/OpenIddict.Models/OpenIddictScope.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictScope.cs similarity index 97% rename from src/OpenIddict.Models/OpenIddictScope.cs rename to src/OpenIddict.EntityFramework.Models/OpenIddictScope.cs index c02b52ae..30967531 100644 --- a/src/OpenIddict.Models/OpenIddictScope.cs +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictScope.cs @@ -6,7 +6,7 @@ using System; -namespace OpenIddict.Models +namespace OpenIddict.EntityFramework.Models { /// /// Represents an OpenIddict scope. diff --git a/src/OpenIddict.EntityFramework.Models/OpenIddictToken.cs b/src/OpenIddict.EntityFramework.Models/OpenIddictToken.cs new file mode 100644 index 00000000..378f8f5c --- /dev/null +++ b/src/OpenIddict.EntityFramework.Models/OpenIddictToken.cs @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; + +namespace OpenIddict.EntityFramework.Models +{ + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken : OpenIddictToken + { + public OpenIddictToken() + { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken where TKey : IEquatable + { + /// + /// Gets or sets the application associated with the current token. + /// + public virtual TApplication Application { get; set; } + + /// + /// Gets or sets the authorization associated with the current token. + /// + public virtual TAuthorization Authorization { get; set; } + + /// + /// Gets or sets the concurrency token. + /// + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the date on which the token + /// will start to be considered valid. + /// + public virtual DateTimeOffset? CreationDate { get; set; } + + /// + /// Gets or sets the date on which the token + /// will no longer be considered valid. + /// + public virtual DateTimeOffset? ExpirationDate { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current token. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the payload of the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be encrypted for security reasons. + /// + public virtual string Payload { get; set; } + + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current token. + /// + public virtual string Properties { get; set; } + + /// + /// Gets or sets the reference identifier associated + /// with the current token, if applicable. + /// Note: this property is only used for reference tokens + /// and may be hashed or encrypted for security reasons. + /// + public virtual string ReferenceId { get; set; } + + /// + /// Gets or sets the status of the current token. + /// + public virtual string Status { get; set; } + + /// + /// Gets or sets the subject associated with the current token. + /// + public virtual string Subject { get; set; } + + /// + /// Gets or sets the type of the current token. + /// + public virtual string Type { get; set; } + } +} diff --git a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj index 98e586ed..73282fc9 100644 --- a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj +++ b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs new file mode 100644 index 00000000..6558f6af --- /dev/null +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Data.Entity; +using JetBrains.Annotations; +using OpenIddict.Core; +using OpenIddict.EntityFramework; +using OpenIddict.EntityFramework.Models; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes the necessary methods required to configure the OpenIddict Entity Framework 6.x services. + /// + public class OpenIddictEntityFrameworkBuilder + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictEntityFrameworkBuilder([NotNull] IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + Services = services; + } + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict Entity Framework 6.x configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictEntityFrameworkBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Configures the OpenIddict Entity Framework 6.x stores to use the specified database context type. + /// + /// The type of the used by OpenIddict. + /// The . + public OpenIddictEntityFrameworkBuilder UseDbContext() + where TContext : DbContext => UseDbContext(typeof(TContext)); + + /// + /// Configures the OpenIddict Entity Framework 6.x stores to use the specified database context type. + /// + /// The type of the used by OpenIddict. + /// The . + public OpenIddictEntityFrameworkBuilder UseDbContext([NotNull] Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!typeof(DbContext).IsAssignableFrom(type)) + { + throw new ArgumentException("The specified type is invalid.", nameof(type)); + } + + return Configure(options => options.ContextType = type); + } + + /// + /// Configures OpenIddict to use the specified entities, derived + /// from the default OpenIddict Entity Framework 6.x entities. + /// + /// The . + public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities() + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() + where TKey : IEquatable + { + Services.Configure(options => + { + options.DefaultApplicationType = typeof(TApplication); + options.DefaultAuthorizationType = typeof(TAuthorization); + options.DefaultScopeType = typeof(TScope); + options.DefaultTokenType = typeof(TToken); + }); + + return this; + } + } +} diff --git a/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs similarity index 79% rename from src/OpenIddict.EntityFramework/OpenIddictExtensions.cs rename to src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs index fbf0dcea..f1f63302 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs @@ -12,37 +12,69 @@ using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenIddict.EntityFramework; -using OpenIddict.Models; +using OpenIddict.EntityFramework.Models; namespace Microsoft.Extensions.DependencyInjection { - public static class OpenIddictExtensions + public static class OpenIddictEntityFrameworkExtensions { /// - /// Registers the Entity Framework 6.x stores. Note: when using the Entity Framework stores, - /// the application MUST be manually registered in the DI container and - /// the entities MUST be derived from the models contained in the OpenIddict.Models package. + /// Registers the Entity Framework 6.x stores services in the DI container and + /// configures OpenIddict to use the Entity Framework 6.x entities by default. /// /// The services builder used by OpenIddict to register new services. /// This extension can be safely called multiple times. - /// The . - public static OpenIddictCoreBuilder AddEntityFrameworkStores([NotNull] this OpenIddictCoreBuilder builder) - where TContext : DbContext + /// The . + public static OpenIddictEntityFrameworkBuilder UseEntityFramework([NotNull] this OpenIddictCoreBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } + builder.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + + builder.ReplaceApplicationStoreResolver() + .ReplaceAuthorizationStoreResolver() + .ReplaceScopeStoreResolver() + .ReplaceTokenStoreResolver(); + builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>)); - return builder.ReplaceApplicationStoreResolver>() - .ReplaceAuthorizationStoreResolver>() - .ReplaceScopeStoreResolver>() - .ReplaceTokenStoreResolver>(); + return new OpenIddictEntityFrameworkBuilder(builder.Services); + } + + /// + /// Registers the Entity Framework 6.x stores services in the DI container and + /// configures OpenIddict to use the Entity Framework 6.x entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the Entity Framework 6.x services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictCoreBuilder UseEntityFramework( + [NotNull] this OpenIddictCoreBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseEntityFramework()); + + return builder; } /// @@ -77,7 +109,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - // Note: unlike Entity Framework Core 1.x/2.x, Entity Framework 6.x + // Note: unlike Entity Framework 6.x 1.x/2.x, Entity Framework 6.x // always throws an exception when using generic types as entity types. // To ensure a better exception is thrown, a manual check is made here. if (typeof(TApplication).IsGenericType) diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkOptions.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkOptions.cs new file mode 100644 index 00000000..058e9f2d --- /dev/null +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkOptions.cs @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Data.Entity; + +namespace OpenIddict.EntityFramework +{ + /// + /// Provides various settings needed to configure + /// the OpenIddict Entity Framework 6.x integration. + /// + public class OpenIddictEntityFrameworkOptions + { + /// + /// Gets or sets the concrete type of the used by the + /// OpenIddict Entity Framework 6.x stores. If this property is not populated, + /// an exception is thrown at runtime when trying to use the stores. + /// + public Type ContextType { get; set; } + } +} diff --git a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictApplicationStoreResolver.cs index b235657c..2f7b7682 100644 --- a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; -using System.Data.Entity; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Exposes a method allowing to resolve an application store. /// - public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver - where TContext : DbContext + public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictApplicationStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified application type is not compatible with the Entity Framework 6.x stores.") - .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in generic ") - .Append("'OpenIddictApplication' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictApplication' entity.") + .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in ") + .Append("'OpenIddictApplication' entity (from the 'OpenIddict.EntityFramework.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictApplication' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework 6.x context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework 6.x stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkStores().UseContext()'.") .ToString()); } @@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework /* TApplication: */ key, /* TAuthorization: */ root.GenericTypeArguments[1], /* TToken: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictAuthorizationStoreResolver.cs index 942dc910..3a03f2b3 100644 --- a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; -using System.Data.Entity; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Exposes a method allowing to resolve an authorization store. /// - public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver - where TContext : DbContext + public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictAuthorizationStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified authorization type is not compatible with the Entity Framework 6.x stores.") - .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in generic ") - .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictAuthorization' entity.") + .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in ") + .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.EntityFramework.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictAuthorization' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework 6.x context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework 6.x stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkStores().UseContext()'.") .ToString()); } @@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework /* TAuthorization: */ key, /* TApplication: */ root.GenericTypeArguments[1], /* TToken: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictScopeStoreResolver.cs index 1744e2d0..e3029112 100644 --- a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictScopeStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; -using System.Data.Entity; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Exposes a method allowing to resolve a scope store. /// - public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver - where TContext : DbContext + public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictScopeStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,15 +54,25 @@ namespace OpenIddict.EntityFramework { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified scope type is not compatible with the Entity Framework 6.x stores.") - .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in generic ") - .Append("'OpenIdScope' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictScope' entity.") + .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in ") + .Append("'OpenIdScope' entity (from the 'OpenIddict.EntityFramework.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictScope' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework 6.x context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework 6.x stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkStores().UseContext()'.") .ToString()); } return typeof(OpenIddictScopeStore<,,>).MakeGenericType( /* TScope: */ key, - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictTokenStoreResolver.cs index aefd8ddd..a560fccf 100644 --- a/src/OpenIddict.EntityFramework/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.EntityFramework/Resolvers/OpenIddictTokenStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; -using System.Data.Entity; using System.Text; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Exposes a method allowing to resolve a token store. /// - public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver - where TContext : DbContext + public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictTokenStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified token type is not compatible with the Entity Framework 6.x stores.") - .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in generic ") - .Append("'OpenIddictToken' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictToken' entity.") + .Append("When enabling the Entity Framework 6.x stores, make sure you use the built-in ") + .Append("'OpenIddictToken' entity (from the 'OpenIddict.EntityFramework.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictToken' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework 6.x context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework 6.x stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkStores().UseContext()'.") .ToString()); } @@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework /* TToken: */ key, /* TApplication: */ root.GenericTypeArguments[1], /* TAuthorization: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs index 4b2b2e06..9efe5086 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs @@ -7,21 +7,25 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Data.Entity; +using System.Data.Entity.Infrastructure; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; -using OpenIddict.Models; -using OpenIddict.Stores; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictApplicationStore : OpenIddictApplicationStore where TContext : DbContext { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. - /// - /// The type of the Entity Framework database context. - /// The type of the entity primary keys. - public class OpenIddictApplicationStore : OpenIddictApplicationStore, - OpenIddictAuthorization, - OpenIddictToken, TContext, TKey> - where TContext : DbContext - where TKey : IEquatable - { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) - { - } - } - - /// - /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Application entity. /// The type of the Authorization entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictApplicationStore : - OpenIddictApplicationStore + public class OpenIddictApplicationStore : IOpenIddictApplicationStore where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of applications that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Applications.LongCountAsync(); /// /// Determines the number of applications that match the specified query. @@ -111,7 +106,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -129,7 +124,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -149,7 +144,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -205,9 +200,158 @@ namespace OpenIddict.EntityFramework Applications.Remove(application); - await Context.SaveChangesAsync(cancellationToken); - transaction?.Commit(); + try + { + await Context.SaveChangesAsync(cancellationToken); + transaction?.Commit(); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString(), exception); + } + } + } + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return (from application in Applications + where application.Id.Equals(key) + select application).FirstOrDefaultAsync(); + } + + /// + /// Retrieves an application using its client identifier. + /// + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public virtual Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } + + return (from application in Applications + where application.ClientId == identifier + select application).FirstOrDefaultAsync(); + } + + /// + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified post_logout_redirect_uri. + /// + public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + // To optimize the efficiency of the query a bit, only applications whose stringified + // PostLogoutRedirectUris contains the specified URL are returned. Once the applications + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. + var applications = await (from application in Applications + where application.PostLogoutRedirectUris.Contains(address) + select application).ToListAsync(cancellationToken); + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var application in applications) + { + foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) + { + // Note: the post_logout_redirect_uri must be compared + // using case-sensitive "Simple String Comparison". + if (string.Equals(uri, address, StringComparison.Ordinal)) + { + builder.Add(application); + + break; + } + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); + } + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified redirect_uri. + /// + public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + // To optimize the efficiency of the query a bit, only applications whose stringified + // RedirectUris property contains the specified URL are returned. Once the applications + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. + var applications = await (from application in Applications + where application.RedirectUris.Contains(address) + select application).ToListAsync(cancellationToken); + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var application in applications) + { + foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken)) + { + // Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. + if (string.Equals(uri, address, StringComparison.Ordinal)) + { + builder.Add(application); + + break; + } + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); } /// @@ -222,7 +366,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -234,6 +378,296 @@ namespace OpenIddict.EntityFramework return query(Applications, state).FirstOrDefaultAsync(cancellationToken); } + /// + /// Retrieves the client identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client identifier associated with the application. + /// + public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientId); + } + + /// + /// Retrieves the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client secret associated with the application. + /// + public virtual ValueTask GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientSecret); + } + + /// + /// Retrieves the client type associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client type of the application (by default, "public"). + /// + public virtual ValueTask GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.Type); + } + + /// + /// Retrieves the consent type associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the consent type of the application (by default, "explicit"). + /// + public virtual ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ConsentType); + } + + /// + /// Retrieves the display name associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the application. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.DisplayName); + } + + /// + /// Retrieves the unique identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the application. + /// + public virtual ValueTask GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(ConvertIdentifierToString(application.Id)); + } + + /// + /// Retrieves the permissions associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the permissions associated with the application. + /// + public virtual ValueTask> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Permissions)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified permissions is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("0347e0aa-3a26-410a-97e8-a83bdeb21a1f", "\x1e", application.Permissions); + var permissions = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.Permissions) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(permissions); + } + + /// + /// Retrieves the logout callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the post_logout_redirect_uri associated with the application. + /// + public virtual ValueTask> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.PostLogoutRedirectUris)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified addresses is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("fb14dfb9-9216-4b77-bfa9-7e85f8201ff4", "\x1e", application.PostLogoutRedirectUris); + var addresses = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.PostLogoutRedirectUris) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(addresses); + } + + /// + /// Retrieves the additional properties associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the application. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(application.Properties)); + } + + /// + /// Retrieves the callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the redirect_uri associated with the application. + /// + public virtual ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.RedirectUris)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified addresses is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("851d6f08-2ee0-4452-bbe5-ab864611ecaa", "\x1e", application.RedirectUris); + var addresses = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.RedirectUris) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(addresses); + } + + /// + /// Instantiates a new application. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated application, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TApplication()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Applications.OrderBy(application => application.Id).AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -246,7 +680,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -258,6 +692,232 @@ namespace OpenIddict.EntityFramework return ImmutableArray.CreateRange(await query(Applications, state).ToListAsync(cancellationToken)); } + /// + /// Sets the client identifier associated with an application. + /// + /// The application. + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientIdAsync([NotNull] TApplication application, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientSecretAsync([NotNull] TApplication application, + [CanBeNull] string secret, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientSecret = secret; + + return Task.CompletedTask; + } + + /// + /// Sets the client type associated with an application. + /// + /// The application. + /// The client type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.Type = type; + + return Task.CompletedTask; + } + + /// + /// Sets the consent type associated with an application. + /// + /// The application. + /// The consent type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetConsentTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ConsentType = type; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with an application. + /// + /// The application. + /// The display name associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TApplication application, + [CanBeNull] string name, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the permissions associated with an application. + /// + /// The application. + /// The permissions associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (permissions.IsDefaultOrEmpty) + { + application.Permissions = null; + + return Task.CompletedTask; + } + + application.Permissions = new JArray(permissions.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the logout callback addresses associated with an application. + /// + /// The application. + /// The logout callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.PostLogoutRedirectUris = null; + + return Task.CompletedTask; + } + + application.PostLogoutRedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with an application. + /// + /// The application. + /// The additional properties associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (properties == null) + { + application.Properties = null; + + return Task.CompletedTask; + } + + application.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the callback addresses associated with an application. + /// + /// The application. + /// The callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.RedirectUris = null; + + return Task.CompletedTask; + } + + application.RedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Updates an existing application. /// @@ -266,7 +926,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -281,7 +941,48 @@ namespace OpenIddict.EntityFramework Context.Entry(application).State = EntityState.Modified; - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs index ff3bea55..89ad29a5 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -7,22 +7,25 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Data.Entity; +using System.Data.Entity.Infrastructure; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; -using OpenIddict.Models; -using OpenIddict.Stores; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictAuthorizationStore : OpenIddictAuthorizationStore where TContext : DbContext { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. - /// - /// The type of the Entity Framework database context. - /// The type of the entity primary keys. - public class OpenIddictAuthorizationStore : OpenIddictAuthorizationStore, - OpenIddictApplication, - OpenIddictToken, TContext, TKey> - where TContext : DbContext - where TKey : IEquatable - { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) - { - } - } - - /// - /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Authorization entity. /// The type of the Application entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictAuthorizationStore : - OpenIddictAuthorizationStore + public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore where TAuthorization : OpenIddictAuthorization, new() where TApplication : OpenIddictApplication, new() where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of authorizations that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of authorizations in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Authorizations.LongCountAsync(); /// /// Determines the number of authorizations that match the specified query. @@ -112,7 +106,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -130,7 +124,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -150,7 +144,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -188,9 +182,189 @@ namespace OpenIddict.EntityFramework Authorizations.Remove(authorization); - await Context.SaveChangesAsync(cancellationToken); - transaction?.Commit(); + try + { + await Context.SaveChangesAsync(cancellationToken); + transaction?.Commit(); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString(), exception); + } + } + } + + /// + /// Retrieves the authorizations corresponding to the specified + /// subject and associated with the application identifier. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the subject/client. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client cannot be null or empty.", nameof(client)); + } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from authorization in Authorizations.Include(authorization => authorization.Application) + where authorization.Application != null && + authorization.Application.Id.Equals(key) && + authorization.Subject == subject + select authorization).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the authorizations matching the specified parameters. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The authorization status. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the criteria. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from authorization in Authorizations.Include(authorization => authorization.Application) + where authorization.Application != null && + authorization.Application.Id.Equals(key) && + authorization.Subject == subject && + authorization.Status == status + select authorization).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the authorizations matching the specified parameters. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The authorization status. + /// The authorization type. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the criteria. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + if (string.IsNullOrEmpty(client)) + { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The type cannot be null or empty.", nameof(type)); } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from authorization in Authorizations.Include(authorization => authorization.Application) + where authorization.Application != null && + authorization.Application.Id.Equals(key) && + authorization.Subject == subject && + authorization.Status == status && + authorization.Type == type + select authorization).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// The unique identifier associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorization corresponding to the identifier. + /// + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return (from authorization in Authorizations.Include(authorization => authorization.Application) + where authorization.Id.Equals(key) + select authorization).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves all the authorizations corresponding to the specified subject. + /// + /// The subject associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the authorizations corresponding to the specified subject. + /// + public virtual async Task> FindBySubjectAsync( + [NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + return ImmutableArray.CreateRange( + await (from authorization in Authorizations.Include(authorization => authorization.Application) + where authorization.Subject == subject + select authorization).ToListAsync(cancellationToken)); } /// @@ -202,7 +376,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the application identifier associated with the authorization. /// - public override async ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -241,7 +415,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -250,7 +424,173 @@ namespace OpenIddict.EntityFramework throw new ArgumentNullException(nameof(query)); } - return query(Authorizations.Include(authorization => authorization.Application), state).FirstOrDefaultAsync(cancellationToken); + return query( + Authorizations.Include(authorization => authorization.Application), state).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the unique identifier associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the authorization. + /// + public virtual ValueTask GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(ConvertIdentifierToString(authorization.Id)); + } + + /// + /// Retrieves the additional properties associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the authorization. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(authorization.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(authorization.Properties)); + } + + /// + /// Retrieves the scopes associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified authorization. + /// + public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(authorization.Scopes)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); + } + + /// + /// Retrieves the status associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified authorization. + /// + public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Status); + } + + /// + /// Retrieves the subject associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified authorization. + /// + public virtual ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Subject); + } + + /// + /// Retrieves the type associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the type associated with the specified authorization. + /// + public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Type); + } + + /// + /// Instantiates a new authorization. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated authorization, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TAuthorization()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Authorizations.Include(authorization => authorization.Application) + .OrderBy(authorization => authorization.Id) + .AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); } /// @@ -265,7 +605,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -285,7 +625,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task PruneAsync(CancellationToken cancellationToken) + public virtual async Task PruneAsync(CancellationToken cancellationToken) { // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -293,14 +633,6 @@ namespace OpenIddict.EntityFramework IList exceptions = null; - IQueryable Query(IQueryable authorizations, int offset) - => (from authorization in authorizations.Include(authorization => authorization.Tokens) - where authorization.Status != OpenIddictConstants.Statuses.Valid || - (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) - orderby authorization.Id - select authorization).Skip(offset).Take(1_000); - DbContextTransaction CreateTransaction() { // Note: relational providers like Sqlite are known to lack proper support @@ -327,8 +659,15 @@ namespace OpenIddict.EntityFramework // and thus prevent them from being concurrently modified outside this block. using (var transaction = CreateTransaction()) { - var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); - if (authorizations.IsEmpty) + var authorizations = + await (from authorization in Authorizations.Include(authorization => authorization.Tokens) + where authorization.Status != OpenIddictConstants.Statuses.Valid || + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && + !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) + orderby authorization.Id + select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + + if (authorizations.Count == 0) { break; } @@ -373,7 +712,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, + public virtual async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (authorization == null) @@ -410,6 +749,129 @@ namespace OpenIddict.EntityFramework } } + /// + /// Sets the additional properties associated with an authorization. + /// + /// The authorization. + /// The additional properties associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (properties == null) + { + authorization.Properties = null; + + return Task.CompletedTask; + } + + authorization.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the scopes associated with an authorization. + /// + /// The authorization. + /// The scopes associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetScopesAsync([NotNull] TAuthorization authorization, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (scopes.IsDefaultOrEmpty) + { + authorization.Scopes = null; + + return Task.CompletedTask; + } + + authorization.Scopes = new JArray(scopes.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with an authorization. + /// + /// The authorization. + /// The status associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetStatusAsync([NotNull] TAuthorization authorization, + [CanBeNull] string status, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with an authorization. + /// + /// The authorization. + /// The subject associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetSubjectAsync([NotNull] TAuthorization authorization, + [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the type associated with an authorization. + /// + /// The authorization. + /// The type associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetTypeAsync([NotNull] TAuthorization authorization, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Type = type; + + return Task.CompletedTask; + } + /// /// Updates an existing authorization. /// @@ -418,7 +880,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -433,7 +895,48 @@ namespace OpenIddict.EntityFramework Context.Entry(authorization).State = EntityState.Modified; - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs index 52a32538..82728ad9 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs @@ -6,81 +6,77 @@ using System; using System.Collections.Immutable; +using System.ComponentModel; using System.Data.Entity; +using System.Data.Entity.Infrastructure; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; -using OpenIddict.Core; -using OpenIddict.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictScopeStore : OpenIddictScopeStore where TContext : DbContext { - public OpenIddictScopeStore( - [NotNull] TContext context, - [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. - /// - /// The type of the Entity Framework database context. - /// The type of the entity primary keys. - public class OpenIddictScopeStore : OpenIddictScopeStore, TContext, TKey> - where TContext : DbContext - where TKey : IEquatable - { - public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) - { - } - } - - /// - /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Scope entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictScopeStore : Stores.OpenIddictScopeStore + public class OpenIddictScopeStore : IOpenIddictScopeStore where TScope : OpenIddictScope, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Scopes => Context.Set(); + private DbSet Scopes => Context.Set(); + + /// + /// Determines the number of scopes that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of scopes in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Scopes.LongCountAsync(); /// /// Determines the number of scopes that match the specified query. @@ -92,7 +88,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -110,7 +106,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -130,7 +126,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -139,7 +135,125 @@ namespace OpenIddict.EntityFramework Scopes.Remove(scope); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Retrieves a scope using its unique identifier. + /// + /// The unique identifier associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the identifier. + /// + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return (from scope in Scopes + where scope.Id.Equals(key) + select scope).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves a scope using its name. + /// + /// The name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the specified name. + /// + public virtual Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); + } + + return (from scope in Scopes + where scope.Name == name + select scope).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves a list of scopes using their name. + /// + /// The names associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes corresponding to the specified names. + /// + public virtual async Task> FindByNamesAsync( + ImmutableArray names, CancellationToken cancellationToken) + { + if (names.Any(name => string.IsNullOrEmpty(name))) + { + throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); + } + + return ImmutableArray.CreateRange( + await (from scope in Scopes + where names.Contains(scope.Name) + select scope).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public virtual async Task> FindByResourceAsync( + [NotNull] string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + // To optimize the efficiency of the query a bit, only scopes whose stringified + // Resources column contains the specified resource are returned. Once the scopes + // are retrieved, a second pass is made to ensure only valid elements are returned. + // Implementers that use this method in a hot path may want to override this method + // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. + var scopes = await (from scope in Scopes + where scope.Resources.Contains(resource) + select scope).ToListAsync(cancellationToken); + + var builder = ImmutableArray.CreateBuilder(); + + foreach (var scope in scopes) + { + var resources = await GetResourcesAsync(scope, cancellationToken); + if (resources.Contains(resource, StringComparer.OrdinalIgnoreCase)) + { + builder.Add(scope); + } + } + + return builder.ToImmutable(); } /// @@ -154,7 +268,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -166,6 +280,182 @@ namespace OpenIddict.EntityFramework return query(Scopes, state).FirstOrDefaultAsync(cancellationToken); } + /// + /// Retrieves the description associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the description associated with the specified scope. + /// + public virtual ValueTask GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Description); + } + + /// + /// Retrieves the display name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the scope. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.DisplayName); + } + + /// + /// Retrieves the unique identifier associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the scope. + /// + public virtual ValueTask GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(ConvertIdentifierToString(scope.Id)); + } + + /// + /// Retrieves the name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the name associated with the specified scope. + /// + public virtual ValueTask GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Name); + } + + /// + /// Retrieves the additional properties associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the scope. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(scope.Properties)); + } + + /// + /// Retrieves the resources associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the resources associated with the scope. + /// + public virtual ValueTask> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Resources)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified resources is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("b6148250-aede-4fb9-a621-07c9bcf238c3", "\x1e", scope.Resources); + var resources = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(scope.Resources) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(resources); + } + + /// + /// Instantiates a new scope. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated scope, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TScope()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Scopes.OrderBy(scope => scope.Id).AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -178,7 +468,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -190,6 +480,125 @@ namespace OpenIddict.EntityFramework return ImmutableArray.CreateRange(await query(Scopes, state).ToListAsync(cancellationToken)); } + /// + /// Sets the description associated with a scope. + /// + /// The scope. + /// The description associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDescriptionAsync([NotNull] TScope scope, [CanBeNull] string description, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Description = description; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with a scope. + /// + /// The scope. + /// The display name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the name associated with a scope. + /// + /// The scope. + /// The name associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Name = name; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a scope. + /// + /// The scope. + /// The additional properties associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (properties == null) + { + scope.Properties = null; + + return Task.CompletedTask; + } + + scope.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the resources associated with a scope. + /// + /// The scope. + /// The resources associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray resources, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (resources.IsDefaultOrEmpty) + { + scope.Resources = null; + + return Task.CompletedTask; + } + + scope.Resources = new JArray(resources.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Updates an existing scope. /// @@ -198,7 +607,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -213,7 +622,48 @@ namespace OpenIddict.EntityFramework Context.Entry(scope).State = EntityState.Modified; - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs index 470e54c7..3a66c3ec 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -7,22 +7,25 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Data.Entity; +using System.Data.Entity.Infrastructure; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; -using OpenIddict.Models; -using OpenIddict.Stores; +using OpenIddict.EntityFramework.Models; namespace OpenIddict.EntityFramework { /// /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictTokenStore : OpenIddictTokenStore where TContext : DbContext { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. - /// - /// The type of the Entity Framework database context. - /// The type of the entity primary keys. - public class OpenIddictTokenStore : OpenIddictTokenStore, - OpenIddictApplication, - OpenIddictAuthorization, TContext, TKey> - where TContext : DbContext - where TKey : IEquatable - { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) - { - } - } - - /// - /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Token entity. /// The type of the Application entity. /// The type of the Authorization entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictTokenStore : - OpenIddictTokenStore + public class OpenIddictTokenStore : IOpenIddictTokenStore where TToken : OpenIddictToken, new() where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of tokens that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Tokens.LongCountAsync(); /// /// Determines the number of tokens that match the specified query. @@ -112,7 +106,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -130,7 +124,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -147,8 +141,10 @@ namespace OpenIddict.EntityFramework /// /// The token to delete. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -157,7 +153,135 @@ namespace OpenIddict.EntityFramework Tokens.Remove(token); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Retrieves the list of tokens corresponding to the specified application identifier. + /// + /// The application identifier associated with the tokens. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the tokens corresponding to the specified application. + /// + public virtual async Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Application != null && + token.Application.Id.Equals(key) + select token).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves the list of tokens corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the tokens. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the tokens corresponding to the specified authorization. + /// + public virtual async Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Authorization != null && + token.Authorization.Id.Equals(key) + select token).ToListAsync(cancellationToken)); + } + + /// + /// Retrieves a token using its unique identifier. + /// + /// The unique identifier associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the token corresponding to the unique identifier. + /// + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var key = ConvertIdentifierFromString(identifier); + + return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Id.Equals(key) + select token).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the list of tokens corresponding to the specified reference identifier. + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// + /// The reference identifier associated with the tokens. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the tokens corresponding to the specified reference identifier. + /// + public virtual Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.ReferenceId == identifier + select token).FirstOrDefaultAsync(cancellationToken); + } + + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the tokens corresponding to the specified subject. + /// + public virtual async Task> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Subject == subject + select token).ToListAsync(cancellationToken)); } /// @@ -169,7 +293,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the application identifier associated with the token. /// - public override async ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -208,7 +332,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -231,7 +355,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization identifier associated with the token. /// - public override async ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -258,6 +382,226 @@ namespace OpenIddict.EntityFramework return ConvertIdentifierToString(token.Authorization.Id); } + /// + /// Retrieves the creation date associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified token. + /// + public virtual ValueTask GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.CreationDate); + } + + /// + /// Retrieves the expiration date associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the expiration date associated with the specified token. + /// + public virtual ValueTask GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ExpirationDate); + } + + /// + /// Retrieves the unique identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the token. + /// + public virtual ValueTask GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(ConvertIdentifierToString(token.Id)); + } + + /// + /// Retrieves the payload associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the payload associated with the specified token. + /// + public virtual ValueTask GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Payload); + } + + /// + /// Retrieves the additional properties associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the token. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(token.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(token.Properties)); + } + + /// + /// Retrieves the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the reference identifier associated with the specified token. + /// + public virtual ValueTask GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ReferenceId); + } + + /// + /// Retrieves the status associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified token. + /// + public virtual ValueTask GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Status); + } + + /// + /// Retrieves the subject associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified token. + /// + public virtual ValueTask GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Subject); + } + + /// + /// Retrieves the token type associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the token type associated with the specified token. + /// + public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Type); + } + + /// + /// Instantiates a new token. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated token, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TToken()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Tokens.Include(token => token.Application) + .Include(token => token.Authorization) + .OrderBy(token => token.Id) + .AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -270,7 +614,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -291,7 +635,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task PruneAsync(CancellationToken cancellationToken) + public virtual async Task PruneAsync(CancellationToken cancellationToken) { // Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -299,13 +643,6 @@ namespace OpenIddict.EntityFramework IList exceptions = null; - IQueryable Query(IQueryable tokens, int offset) - => (from token in tokens - where token.ExpirationDate < DateTimeOffset.UtcNow || - token.Status != OpenIddictConstants.Statuses.Valid - orderby token.Id - select token).Skip(offset).Take(1_000); - DbContextTransaction CreateTransaction() { // Note: relational providers like Sqlite are known to lack proper support @@ -332,8 +669,14 @@ namespace OpenIddict.EntityFramework // and thus prevent them from being concurrently modified outside this block. using (var transaction = CreateTransaction()) { - var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); - if (tokens.IsEmpty) + var tokens = + await (from token in Tokens + where token.ExpirationDate < DateTimeOffset.UtcNow || + token.Status != OpenIddictConstants.Statuses.Valid + orderby token.Id + select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + + if (tokens.Count == 0) { break; } @@ -373,7 +716,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetApplicationIdAsync([NotNull] TToken token, + public virtual async Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (token == null) @@ -419,7 +762,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetAuthorizationIdAsync([NotNull] TToken token, + public virtual async Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (token == null) @@ -456,6 +799,200 @@ namespace OpenIddict.EntityFramework } } + /// + /// Sets the creation date associated with a token. + /// + /// The token. + /// The creation date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetCreationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.CreationDate = date; + + return Task.CompletedTask; + } + + /// + /// Sets the expiration date associated with a token. + /// + /// The token. + /// The expiration date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetExpirationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ExpirationDate = date; + + return Task.CompletedTask; + } + + /// + /// Sets the payload associated with a token. + /// + /// The token. + /// The payload associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Payload = payload; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a token. + /// + /// The token. + /// The additional properties associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (properties == null) + { + token.Properties = null; + + return Task.CompletedTask; + } + + token.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// The token. + /// The reference identifier associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetReferenceIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ReferenceId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with a token. + /// + /// The token. + /// The status associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetStatusAsync([NotNull] TToken token, [CanBeNull] string status, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + token.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with a token. + /// + /// The token. + /// The subject associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetSubjectAsync([NotNull] TToken token, [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + token.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the token type associated with a token. + /// + /// The token. + /// The token type associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetTokenTypeAsync([NotNull] TToken token, [CanBeNull] string type, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); + } + + token.Type = type; + + return Task.CompletedTask; + } + /// /// Updates an existing token. /// @@ -464,7 +1001,7 @@ namespace OpenIddict.EntityFramework /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -479,7 +1016,48 @@ namespace OpenIddict.EntityFramework Context.Entry(token).State = EntityState.Modified; - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj new file mode 100644 index 00000000..fe2ed955 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj @@ -0,0 +1,15 @@ + + + + + + netstandard2.0 + + + + Relational entities for the Entity Framework Core stores. + Kévin Chalet + aspnetcore;authentication;jwt;openidconnect;openiddict;security + + + diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs similarity index 98% rename from src/OpenIddict.Models/OpenIddictApplication.cs rename to src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs index 0665b573..9b11b33b 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs @@ -7,7 +7,7 @@ using System; using System.Collections.Generic; -namespace OpenIddict.Models +namespace OpenIddict.EntityFrameworkCore.Models { /// /// Represents an OpenIddict application. diff --git a/src/OpenIddict.Models/OpenIddictAuthorization.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictAuthorization.cs similarity index 95% rename from src/OpenIddict.Models/OpenIddictAuthorization.cs rename to src/OpenIddict.EntityFrameworkCore.Models/OpenIddictAuthorization.cs index b975e9be..608080b6 100644 --- a/src/OpenIddict.Models/OpenIddictAuthorization.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictAuthorization.cs @@ -7,7 +7,7 @@ using System; using System.Collections.Generic; -namespace OpenIddict.Models +namespace OpenIddict.EntityFrameworkCore.Models { /// /// Represents an OpenIddict authorization. @@ -72,8 +72,7 @@ namespace OpenIddict.Models public virtual string Subject { get; set; } /// - /// Gets or sets the list of tokens - /// associated with the current authorization. + /// Gets the list of tokens associated with the current authorization. /// public virtual IList Tokens { get; } = new List(); diff --git a/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictScope.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictScope.cs new file mode 100644 index 00000000..b28ba8a0 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictScope.cs @@ -0,0 +1,69 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; + +namespace OpenIddict.EntityFrameworkCore.Models +{ + /// + /// Represents an OpenIddict scope. + /// + public class OpenIddictScope : OpenIddictScope + { + public OpenIddictScope() + { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict scope. + /// + public class OpenIddictScope where TKey : IEquatable + { + /// + /// Gets or sets the concurrency token. + /// + public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Gets or sets the public description + /// associated with the current scope. + /// + public virtual string Description { get; set; } + + /// + /// Gets or sets the display name + /// associated with the current scope. + /// + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current scope. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the unique name + /// associated with the current scope. + /// + public virtual string Name { get; set; } + + /// + /// Gets or sets the additional properties serialized as a JSON object, + /// or null if no bag was associated with the current scope. + /// + public virtual string Properties { get; set; } + + /// + /// Gets or sets the resources associated with the + /// current scope, serialized as a JSON array. + /// + public virtual string Resources { get; set; } + } +} diff --git a/src/OpenIddict.Models/OpenIddictToken.cs b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictToken.cs similarity index 98% rename from src/OpenIddict.Models/OpenIddictToken.cs rename to src/OpenIddict.EntityFrameworkCore.Models/OpenIddictToken.cs index 0242874b..92cca5d3 100644 --- a/src/OpenIddict.Models/OpenIddictToken.cs +++ b/src/OpenIddict.EntityFrameworkCore.Models/OpenIddictToken.cs @@ -6,7 +6,7 @@ using System; -namespace OpenIddict.Models +namespace OpenIddict.EntityFrameworkCore.Models { /// /// Represents an OpenIddict token. diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj index 46042f4d..b78b2cff 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs new file mode 100644 index 00000000..4e291063 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs @@ -0,0 +1,123 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using OpenIddict.Core; +using OpenIddict.EntityFrameworkCore; +using OpenIddict.EntityFrameworkCore.Models; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Exposes the necessary methods required to configure the OpenIddict Entity Framework Core services. + /// + public class OpenIddictEntityFrameworkCoreBuilder + { + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictEntityFrameworkCoreBuilder([NotNull] IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + Services = services; + } + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict Entity Framework Core configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictEntityFrameworkCoreBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Configures the OpenIddict Entity Framework Core stores to use the specified database context type. + /// + /// The type of the used by OpenIddict. + /// The . + public OpenIddictEntityFrameworkCoreBuilder UseDbContext() + where TContext : DbContext => UseDbContext(typeof(TContext)); + + /// + /// Configures the OpenIddict Entity Framework Core stores to use the specified database context type. + /// + /// The type of the used by OpenIddict. + /// The . + public OpenIddictEntityFrameworkCoreBuilder UseDbContext([NotNull] Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (!typeof(DbContext).IsAssignableFrom(type)) + { + throw new ArgumentException("The specified type is invalid.", nameof(type)); + } + + return Configure(options => options.ContextType = type); + } + + /// + /// Configures OpenIddict to use the default OpenIddict + /// Entity Framework Core entities, with the specified key type. + /// + /// The . + public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities() + where TKey : IEquatable + => ReplaceDefaultEntities, + OpenIddictAuthorization, + OpenIddictScope, + OpenIddictToken, TKey>(); + + /// + /// Configures OpenIddict to use the specified entities, derived + /// from the default OpenIddict Entity Framework Core entities. + /// + /// The . + public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities() + where TApplication : OpenIddictApplication, new() + where TAuthorization : OpenIddictAuthorization, new() + where TScope : OpenIddictScope, new() + where TToken : OpenIddictToken, new() + where TKey : IEquatable + { + Services.Configure(options => + { + options.DefaultApplicationType = typeof(TApplication); + options.DefaultAuthorizationType = typeof(TAuthorization); + options.DefaultScopeType = typeof(TScope); + options.DefaultTokenType = typeof(TToken); + }); + + return this; + } + } +} diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs similarity index 84% rename from src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs rename to src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs index 77459060..bcbceab6 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs @@ -9,7 +9,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { @@ -17,14 +17,14 @@ namespace OpenIddict.EntityFrameworkCore /// Represents a model customizer able to register the entity sets /// required by the OpenIddict stack in an Entity Framework context. /// - public class OpenIddictCustomizer : RelationalModelCustomizer + public class OpenIddictEntityFrameworkCoreCustomizer : RelationalModelCustomizer where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TScope : OpenIddictScope, new() where TToken : OpenIddictToken, new() where TKey : IEquatable { - public OpenIddictCustomizer([NotNull] ModelCustomizerDependencies dependencies) + public OpenIddictEntityFrameworkCoreCustomizer([NotNull] ModelCustomizerDependencies dependencies) : base(dependencies) { } diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs similarity index 80% rename from src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs rename to src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs index 5145552e..7810b622 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -10,36 +10,69 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection.Extensions; using OpenIddict.EntityFrameworkCore; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace Microsoft.Extensions.DependencyInjection { - public static class OpenIddictExtensions + public static class OpenIddictEntityFrameworkCoreExtensions { /// - /// Registers the Entity Framework Core stores. Note: when using the Entity Framework Core stores, - /// the entities MUST be derived from the models contained in the OpenIddict.Models package. + /// Registers the Entity Framework Core stores services in the DI container and + /// configures OpenIddict to use the Entity Framework Core entities by default. /// /// The services builder used by OpenIddict to register new services. /// This extension can be safely called multiple times. - /// The . - public static OpenIddictCoreBuilder AddEntityFrameworkCoreStores([NotNull] this OpenIddictCoreBuilder builder) - where TContext : DbContext + /// The . + public static OpenIddictEntityFrameworkCoreBuilder UseEntityFrameworkCore([NotNull] this OpenIddictCoreBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } + builder.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + + builder.ReplaceApplicationStoreResolver() + .ReplaceAuthorizationStoreResolver() + .ReplaceScopeStoreResolver() + .ReplaceTokenStoreResolver(); + builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>)); - return builder.ReplaceApplicationStoreResolver>() - .ReplaceAuthorizationStoreResolver>() - .ReplaceScopeStoreResolver>() - .ReplaceTokenStoreResolver>(); + return new OpenIddictEntityFrameworkCoreBuilder(builder.Services); + } + + /// + /// Registers the Entity Framework Core stores services in the DI container and + /// configures OpenIddict to use the Entity Framework Core entities by default. + /// + /// The services builder used by OpenIddict to register new services. + /// The configuration delegate used to configure the Entity Framework Core services. + /// This extension can be safely called multiple times. + /// The . + public static OpenIddictCoreBuilder UseEntityFrameworkCore( + [NotNull] this OpenIddictCoreBuilder builder, + [NotNull] Action configuration) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(builder.UseEntityFrameworkCore()); + + return builder; } /// @@ -85,7 +118,8 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - return builder.ReplaceService>(); + return builder.ReplaceService>(); } /// @@ -184,8 +218,7 @@ namespace Microsoft.Extensions.DependencyInjection entity.HasMany(authorization => authorization.Tokens) .WithOne(token => token.Authorization) .HasForeignKey("AuthorizationId") - .IsRequired(required: false) - .OnDelete(DeleteBehavior.Cascade); + .IsRequired(required: false); entity.ToTable("OpenIddictAuthorizations"); }); diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs new file mode 100644 index 00000000..e27f0227 --- /dev/null +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs @@ -0,0 +1,25 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict.EntityFrameworkCore +{ + /// + /// Provides various settings needed to configure + /// the OpenIddict Entity Framework Core integration. + /// + public class OpenIddictEntityFrameworkCoreOptions + { + /// + /// Gets or sets the concrete type of the used by the + /// OpenIddict Entity Framework Core stores. If this property is not populated, + /// an exception is thrown at runtime when trying to use the stores. + /// + public Type ContextType { get; set; } + } +} diff --git a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictApplicationStoreResolver.cs index 9f5c5e46..a2bc512c 100644 --- a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; using System.Text; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Exposes a method allowing to resolve an application store. /// - public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver - where TContext : DbContext + public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictApplicationStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +54,19 @@ namespace OpenIddict.EntityFrameworkCore { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified application type is not compatible with the Entity Framework Core stores.") - .Append("When enabling the Entity Framework Core stores, make sure you use the built-in generic ") - .Append("'OpenIddictApplication' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictApplication' entity.") + .Append("When enabling the Entity Framework Core stores, make sure you use the built-in ") + .Append("'OpenIddictApplication' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictApplication' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework Core context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework Core stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkCoreStores().UseContext()'.") .ToString()); } @@ -55,7 +74,7 @@ namespace OpenIddict.EntityFrameworkCore /* TApplication: */ key, /* TAuthorization: */ root.GenericTypeArguments[1], /* TToken: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictAuthorizationStoreResolver.cs index a0fae2c4..400a01ef 100644 --- a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -1,26 +1,36 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; using System.Text; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Exposes a method allowing to resolve an authorization store. /// - public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver - where TContext : DbContext + public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictAuthorizationStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +55,19 @@ namespace OpenIddict.EntityFrameworkCore { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified authorization type is not compatible with the Entity Framework Core stores.") - .Append("When enabling the Entity Framework Core stores, make sure you use the built-in generic ") - .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictAuthorization' entity.") + .Append("When enabling the Entity Framework Core stores, make sure you use the built-in ") + .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictAuthorization' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework Core context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework Core stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkCoreStores().UseContext()'.") .ToString()); } @@ -55,7 +75,7 @@ namespace OpenIddict.EntityFrameworkCore /* TAuthorization: */ key, /* TApplication: */ root.GenericTypeArguments[1], /* TToken: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictScopeStoreResolver.cs index d7dc2ed1..53b02961 100644 --- a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictScopeStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; using System.Text; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Exposes a method allowing to resolve a scope store. /// - public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver - where TContext : DbContext + public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictScopeStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,15 +54,25 @@ namespace OpenIddict.EntityFrameworkCore { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified scope type is not compatible with the Entity Framework Core stores.") - .Append("When enabling the Entity Framework Core stores, make sure you use the built-in generic ") - .Append("'OpenIdScope' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictScope' entity.") + .Append("When enabling the Entity Framework Core stores, make sure you use the built-in ") + .Append("'OpenIdScope' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictScope' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework Core context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework Core stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkCoreStores().UseContext()'.") .ToString()); } return typeof(OpenIddictScopeStore<,,>).MakeGenericType( /* TScope: */ key, - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictTokenStoreResolver.cs index f8f5da53..fd00e308 100644 --- a/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictTokenStoreResolver.cs @@ -1,26 +1,35 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using System.Collections.Concurrent; using System.Text; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Exposes a method allowing to resolve a token store. /// - public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver - where TContext : DbContext + public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; - public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) + public OpenIddictTokenStoreResolver( + [NotNull] IOptionsMonitor options, + [NotNull] IServiceProvider provider) { + _options = options; _provider = provider; } @@ -45,9 +54,19 @@ namespace OpenIddict.EntityFrameworkCore { throw new InvalidOperationException(new StringBuilder() .AppendLine("The specified token type is not compatible with the Entity Framework Core stores.") - .Append("When enabling the Entity Framework Core stores, make sure you use the built-in generic ") - .Append("'OpenIddictToken' entity (from the 'OpenIddict.Models' package) or a custom entity ") - .Append("that inherits from the generic 'OpenIddictToken' entity.") + .Append("When enabling the Entity Framework Core stores, make sure you use the built-in ") + .Append("'OpenIddictToken' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ") + .Append("or a custom entity that inherits from the generic 'OpenIddictToken' entity.") + .ToString()); + } + + var context = _options.CurrentValue.ContextType; + if (context == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("No Entity Framework Core context was specified in the OpenIddict options.") + .Append("To configure the OpenIddict Entity Framework Core stores to use a specific 'DbContext', ") + .Append("use 'options.AddEntityFrameworkCoreStores().UseContext()'.") .ToString()); } @@ -55,7 +74,7 @@ namespace OpenIddict.EntityFrameworkCore /* TToken: */ key, /* TApplication: */ root.GenericTypeArguments[1], /* TAuthorization: */ root.GenericTypeArguments[2], - /* TContext: */ typeof(TContext), + /* TContext: */ context, /* TKey: */ root.GenericTypeArguments[0]); }); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index a0449c42..cde98d9c 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -16,14 +18,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; -using OpenIddict.Models; -using OpenIddict.Stores; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictApplicationStore : OpenIddictApplicationStore where TContext : DbContext { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. /// The type of the entity primary keys. @@ -49,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore where TContext : DbContext where TKey : IEquatable { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the applications stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Application entity. /// The type of the Authorization entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictApplicationStore : - OpenIddictApplicationStore + public class OpenIddictApplicationStore : IOpenIddictApplicationStore where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of applications that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Applications.LongCountAsync(); /// /// Determines the number of applications that match the specified query. @@ -113,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -131,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -151,7 +162,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -229,8 +240,19 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(application); - await Context.SaveChangesAsync(cancellationToken); - transaction?.Commit(); + try + { + await Context.SaveChangesAsync(cancellationToken); + transaction?.Commit(); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString(), exception); + } } } @@ -243,7 +265,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// - public override Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -272,7 +294,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// - public override Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -301,7 +323,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public override async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -354,7 +376,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified redirect_uri. /// - public override async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -410,7 +432,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -422,6 +444,296 @@ namespace OpenIddict.EntityFrameworkCore return query(Applications.AsTracking(), state).FirstOrDefaultAsync(cancellationToken); } + /// + /// Retrieves the client identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client identifier associated with the application. + /// + public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientId); + } + + /// + /// Retrieves the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client secret associated with the application. + /// + public virtual ValueTask GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ClientSecret); + } + + /// + /// Retrieves the client type associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client type of the application (by default, "public"). + /// + public virtual ValueTask GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.Type); + } + + /// + /// Retrieves the consent type associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the consent type of the application (by default, "explicit"). + /// + public virtual ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.ConsentType); + } + + /// + /// Retrieves the display name associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the application. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(application.DisplayName); + } + + /// + /// Retrieves the unique identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the application. + /// + public virtual ValueTask GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return new ValueTask(ConvertIdentifierToString(application.Id)); + } + + /// + /// Retrieves the permissions associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the permissions associated with the application. + /// + public virtual ValueTask> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Permissions)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified permissions is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("0347e0aa-3a26-410a-97e8-a83bdeb21a1f", "\x1e", application.Permissions); + var permissions = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.Permissions) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(permissions); + } + + /// + /// Retrieves the logout callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the post_logout_redirect_uri associated with the application. + /// + public virtual ValueTask> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.PostLogoutRedirectUris)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified addresses is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("fb14dfb9-9216-4b77-bfa9-7e85f8201ff4", "\x1e", application.PostLogoutRedirectUris); + var addresses = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.PostLogoutRedirectUris) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(addresses); + } + + /// + /// Retrieves the additional properties associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the application. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(application.Properties)); + } + + /// + /// Retrieves the callback addresses associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the redirect_uri associated with the application. + /// + public virtual ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(application.RedirectUris)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified addresses is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("851d6f08-2ee0-4452-bbe5-ab864611ecaa", "\x1e", application.RedirectUris); + var addresses = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(application.RedirectUris) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(addresses); + } + + /// + /// Instantiates a new application. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated application, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TApplication()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Applications.OrderBy(application => application.Id).AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -434,7 +746,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -446,6 +758,232 @@ namespace OpenIddict.EntityFrameworkCore return ImmutableArray.CreateRange(await query(Applications.AsTracking(), state).ToListAsync(cancellationToken)); } + /// + /// Sets the client identifier associated with an application. + /// + /// The application. + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientIdAsync([NotNull] TApplication application, + [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the client secret associated with an application. + /// Note: depending on the manager used to create the application, + /// the client secret may be hashed for security reasons. + /// + /// The application. + /// The client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientSecretAsync([NotNull] TApplication application, + [CanBeNull] string secret, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ClientSecret = secret; + + return Task.CompletedTask; + } + + /// + /// Sets the client type associated with an application. + /// + /// The application. + /// The client type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.Type = type; + + return Task.CompletedTask; + } + + /// + /// Sets the consent type associated with an application. + /// + /// The application. + /// The consent type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetConsentTypeAsync([NotNull] TApplication application, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.ConsentType = type; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with an application. + /// + /// The application. + /// The display name associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TApplication application, + [CanBeNull] string name, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + application.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the permissions associated with an application. + /// + /// The application. + /// The permissions associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (permissions.IsDefaultOrEmpty) + { + application.Permissions = null; + + return Task.CompletedTask; + } + + application.Permissions = new JArray(permissions.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the logout callback addresses associated with an application. + /// + /// The application. + /// The logout callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.PostLogoutRedirectUris = null; + + return Task.CompletedTask; + } + + application.PostLogoutRedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with an application. + /// + /// The application. + /// The additional properties associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (properties == null) + { + application.Properties = null; + + return Task.CompletedTask; + } + + application.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the callback addresses associated with an application. + /// + /// The application. + /// The callback addresses associated with the application + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetRedirectUrisAsync([NotNull] TApplication application, + ImmutableArray addresses, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (addresses.IsDefaultOrEmpty) + { + application.RedirectUris = null; + + return Task.CompletedTask; + } + + application.RedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Updates an existing application. /// @@ -454,7 +992,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -469,7 +1007,48 @@ namespace OpenIddict.EntityFrameworkCore Context.Update(application); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The application was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the application from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index 6ec354a7..32c634c5 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -16,15 +18,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; -using OpenIddict.Models; -using OpenIddict.Stores; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictAuthorizationStore : OpenIddictAuthorizationStore where TContext : DbContext { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. /// The type of the entity primary keys. @@ -50,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore where TContext : DbContext where TKey : IEquatable { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Authorization entity. /// The type of the Application entity. /// The type of the Token entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictAuthorizationStore : - OpenIddictAuthorizationStore + public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore where TAuthorization : OpenIddictAuthorization, new() where TApplication : OpenIddictApplication, new() where TToken : OpenIddictToken, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of authorizations that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of authorizations in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Authorizations.LongCountAsync(); /// /// Determines the number of authorizations that match the specified query. @@ -114,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -132,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -152,7 +162,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -206,8 +216,19 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(authorization); - await Context.SaveChangesAsync(cancellationToken); - transaction?.Commit(); + try + { + await Context.SaveChangesAsync(cancellationToken); + transaction?.Commit(); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString(), exception); + } } } @@ -222,7 +243,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorizations corresponding to the subject/client. /// - public override async Task> FindAsync( + public virtual async Task> FindAsync( [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) @@ -268,7 +289,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorizations corresponding to the criteria. /// - public override async Task> FindAsync( + public virtual async Task> FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, CancellationToken cancellationToken) { @@ -321,7 +342,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorizations corresponding to the criteria. /// - public override async Task> FindAsync( + public virtual async Task> FindAsync( [NotNull] string subject, [NotNull] string client, [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) { @@ -378,7 +399,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// - public override Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -409,7 +430,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorizations corresponding to the specified subject. /// - public override async Task> FindBySubjectAsync( + public virtual async Task> FindBySubjectAsync( [NotNull] string subject, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) @@ -441,7 +462,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the application identifier associated with the authorization. /// - public override async ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -480,7 +501,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -494,6 +515,171 @@ namespace OpenIddict.EntityFrameworkCore .AsTracking(), state).FirstOrDefaultAsync(cancellationToken); } + /// + /// Retrieves the unique identifier associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the authorization. + /// + public virtual ValueTask GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(ConvertIdentifierToString(authorization.Id)); + } + + /// + /// Retrieves the additional properties associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the authorization. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(authorization.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(authorization.Properties)); + } + + /// + /// Retrieves the scopes associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified authorization. + /// + public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (string.IsNullOrEmpty(authorization.Scopes)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + return new ValueTask>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); + } + + /// + /// Retrieves the status associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified authorization. + /// + public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Status); + } + + /// + /// Retrieves the subject associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified authorization. + /// + public virtual ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Subject); + } + + /// + /// Retrieves the type associated with an authorization. + /// + /// The authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the type associated with the specified authorization. + /// + public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + return new ValueTask(authorization.Type); + } + + /// + /// Instantiates a new authorization. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated authorization, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TAuthorization()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Authorizations.Include(authorization => authorization.Application) + .OrderBy(authorization => authorization.Id) + .AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -506,7 +692,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -527,7 +713,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task PruneAsync(CancellationToken cancellationToken) + public virtual async Task PruneAsync(CancellationToken cancellationToken) { // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -535,14 +721,6 @@ namespace OpenIddict.EntityFrameworkCore IList exceptions = null; - IQueryable Query(IQueryable authorizations, int offset) - => (from authorization in authorizations.Include(authorization => authorization.Tokens).AsTracking() - where authorization.Status != OpenIddictConstants.Statuses.Valid || - (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) - orderby authorization.Id - select authorization).Skip(offset).Take(1_000); - async Task CreateTransactionAsync() { // Note: transactions that specify an explicit isolation level are only supported by @@ -579,8 +757,15 @@ namespace OpenIddict.EntityFrameworkCore // and thus prevent them from being concurrently modified outside this block. using (var transaction = await CreateTransactionAsync()) { - var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); - if (authorizations.IsEmpty) + var authorizations = + await (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking() + where authorization.Status != OpenIddictConstants.Statuses.Valid || + (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && + !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) + orderby authorization.Id + select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + + if (authorizations.Count == 0) { break; } @@ -625,7 +810,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, + public virtual async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (authorization == null) @@ -662,6 +847,129 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + /// Sets the additional properties associated with an authorization. + /// + /// The authorization. + /// The additional properties associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (properties == null) + { + authorization.Properties = null; + + return Task.CompletedTask; + } + + authorization.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the scopes associated with an authorization. + /// + /// The authorization. + /// The scopes associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetScopesAsync([NotNull] TAuthorization authorization, + ImmutableArray scopes, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + if (scopes.IsDefaultOrEmpty) + { + authorization.Scopes = null; + + return Task.CompletedTask; + } + + authorization.Scopes = new JArray(scopes.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with an authorization. + /// + /// The authorization. + /// The status associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetStatusAsync([NotNull] TAuthorization authorization, + [CanBeNull] string status, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with an authorization. + /// + /// The authorization. + /// The subject associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetSubjectAsync([NotNull] TAuthorization authorization, + [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the type associated with an authorization. + /// + /// The authorization. + /// The type associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetTypeAsync([NotNull] TAuthorization authorization, + [CanBeNull] string type, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + authorization.Type = type; + + return Task.CompletedTask; + } + /// /// Updates an existing authorization. /// @@ -670,7 +978,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { @@ -685,7 +993,48 @@ namespace OpenIddict.EntityFrameworkCore Context.Update(authorization); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The authorization was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the authorization from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index 2cbe2145..40bdb38b 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -6,33 +6,36 @@ using System; using System.Collections.Immutable; +using System.ComponentModel; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; -using OpenIddict.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictScopeStore : OpenIddictScopeStore where TContext : DbContext { - public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. /// The type of the entity primary keys. @@ -40,44 +43,54 @@ namespace OpenIddict.EntityFrameworkCore where TContext : DbContext where TKey : IEquatable { - public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Scope entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictScopeStore : Stores.OpenIddictScopeStore + public class OpenIddictScopeStore : IOpenIddictScopeStore where TScope : OpenIddictScope, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Scopes => Context.Set(); + private DbSet Scopes => Context.Set(); + + /// + /// Determines the number of scopes that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of scopes in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Scopes.LongCountAsync(); /// /// Determines the number of scopes that match the specified query. @@ -89,7 +102,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -107,7 +120,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -127,7 +140,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -136,7 +149,18 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(scope); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString(), exception); + } } /// @@ -148,7 +172,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the identifier. /// - public override Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -177,7 +201,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the specified name. /// - public override Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) + public virtual Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(name)) { @@ -206,7 +230,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the scopes corresponding to the specified names. /// - public override async Task> FindByNamesAsync( + public virtual async Task> FindByNamesAsync( ImmutableArray names, CancellationToken cancellationToken) { if (names.Any(name => string.IsNullOrEmpty(name))) @@ -236,7 +260,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the scopes associated with the specified resource. /// - public override async Task> FindByResourceAsync( + public virtual async Task> FindByResourceAsync( [NotNull] string resource, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(resource)) @@ -285,7 +309,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -297,6 +321,182 @@ namespace OpenIddict.EntityFrameworkCore return query(Scopes.AsTracking(), state).FirstOrDefaultAsync(cancellationToken); } + /// + /// Retrieves the description associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the description associated with the specified scope. + /// + public virtual ValueTask GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Description); + } + + /// + /// Retrieves the display name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the display name associated with the scope. + /// + public virtual ValueTask GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.DisplayName); + } + + /// + /// Retrieves the unique identifier associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the scope. + /// + public virtual ValueTask GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(ConvertIdentifierToString(scope.Id)); + } + + /// + /// Retrieves the name associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the name associated with the specified scope. + /// + public virtual ValueTask GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + return new ValueTask(scope.Name); + } + + /// + /// Retrieves the additional properties associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the scope. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(scope.Properties)); + } + + /// + /// Retrieves the resources associated with a scope. + /// + /// The scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the resources associated with the scope. + /// + public virtual ValueTask> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (string.IsNullOrEmpty(scope.Resources)) + { + return new ValueTask>(ImmutableArray.Create()); + } + + // Note: parsing the stringified resources is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("b6148250-aede-4fb9-a621-07c9bcf238c3", "\x1e", scope.Resources); + var resources = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(scope.Resources) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(resources); + } + + /// + /// Instantiates a new scope. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated scope, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TScope()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Scopes.OrderBy(scope => scope.Id).AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -309,7 +509,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -321,6 +521,125 @@ namespace OpenIddict.EntityFrameworkCore return ImmutableArray.CreateRange(await query(Scopes.AsTracking(), state).ToListAsync(cancellationToken)); } + /// + /// Sets the description associated with a scope. + /// + /// The scope. + /// The description associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDescriptionAsync([NotNull] TScope scope, [CanBeNull] string description, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Description = description; + + return Task.CompletedTask; + } + + /// + /// Sets the display name associated with a scope. + /// + /// The scope. + /// The display name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetDisplayNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.DisplayName = name; + + return Task.CompletedTask; + } + + /// + /// Sets the name associated with a scope. + /// + /// The scope. + /// The name associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + scope.Name = name; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a scope. + /// + /// The scope. + /// The additional properties associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (properties == null) + { + scope.Properties = null; + + return Task.CompletedTask; + } + + scope.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the resources associated with a scope. + /// + /// The scope. + /// The resources associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray resources, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + if (resources.IsDefaultOrEmpty) + { + scope.Resources = null; + + return Task.CompletedTask; + } + + scope.Resources = new JArray(resources.ToArray()).ToString(Formatting.None); + + return Task.CompletedTask; + } + /// /// Updates an existing scope. /// @@ -329,7 +648,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) { if (scope == null) { @@ -344,7 +663,48 @@ namespace OpenIddict.EntityFrameworkCore Context.Update(scope); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The scope was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the scope from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 8ecb348b..ee7db43b 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -7,8 +7,10 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.ComponentModel; using System.Data; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -16,16 +18,15 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.Extensions.Caching.Memory; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; -using OpenIddict.Core; -using OpenIddict.Models; -using OpenIddict.Stores; +using OpenIddict.EntityFrameworkCore.Models; namespace OpenIddict.EntityFrameworkCore { /// /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. public class OpenIddictTokenStore : OpenIddictTokenStore where TContext : DbContext { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Entity Framework database context. /// The type of the entity primary keys. @@ -51,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore where TContext : DbContext where TKey : IEquatable { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(context, cache) + public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context) + : base(cache, context) { } } /// /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this class can only be used with the default OpenIddict entities. /// /// The type of the Token entity. /// The type of the Application entity. /// The type of the Authorization entity. /// The type of the Entity Framework database context. /// The type of the entity primary keys. - public class OpenIddictTokenStore : - OpenIddictTokenStore + public class OpenIddictTokenStore : IOpenIddictTokenStore where TToken : OpenIddictToken, new() where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TContext : DbContext where TKey : IEquatable { - public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) - : base(cache) + public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - + Cache = cache; Context = context; } + /// + /// Gets the memory cached associated with the current store. + /// + protected IMemoryCache Cache { get; } + /// /// Gets the database context associated with the current store. /// - protected virtual TContext Context { get; } + protected TContext Context { get; } /// /// Gets the database set corresponding to the entity. /// - protected DbSet Applications => Context.Set(); + private DbSet Applications => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Authorizations => Context.Set(); + private DbSet Authorizations => Context.Set(); /// /// Gets the database set corresponding to the entity. /// - protected DbSet Tokens => Context.Set(); + private DbSet Tokens => Context.Set(); + + /// + /// Determines the number of tokens that exist in the database. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the number of applications in the database. + /// + public virtual Task CountAsync(CancellationToken cancellationToken) + => Tokens.LongCountAsync(); /// /// Determines the number of tokens that match the specified query. @@ -115,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// - public override Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -133,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -150,8 +159,10 @@ namespace OpenIddict.EntityFrameworkCore /// /// The token to delete. /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -160,7 +171,18 @@ namespace OpenIddict.EntityFrameworkCore Context.Remove(token); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString(), exception); + } } /// @@ -172,7 +194,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified application. /// - public override async Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual async Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -210,7 +232,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified authorization. /// - public override async Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual async Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -248,7 +270,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the token corresponding to the unique identifier. /// - public override Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -281,7 +303,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified reference identifier. /// - public override Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -313,7 +335,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified subject. /// - public override async Task> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) + public virtual async Task> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(subject)) { @@ -345,7 +367,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the application identifier associated with the token. /// - public override async ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -384,7 +406,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the first element returned when executing the query. /// - public override Task GetAsync( + public virtual Task GetAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -408,7 +430,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization identifier associated with the token. /// - public override async ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -435,6 +457,226 @@ namespace OpenIddict.EntityFrameworkCore return ConvertIdentifierToString(token.Authorization.Id); } + /// + /// Retrieves the creation date associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the creation date associated with the specified token. + /// + public virtual ValueTask GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.CreationDate); + } + + /// + /// Retrieves the expiration date associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the expiration date associated with the specified token. + /// + public virtual ValueTask GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ExpirationDate); + } + + /// + /// Retrieves the unique identifier associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the token. + /// + public virtual ValueTask GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(ConvertIdentifierToString(token.Id)); + } + + /// + /// Retrieves the payload associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the payload associated with the specified token. + /// + public virtual ValueTask GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Payload); + } + + /// + /// Retrieves the additional properties associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the additional properties associated with the token. + /// + public virtual ValueTask GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(token.Properties)) + { + return new ValueTask(new JObject()); + } + + return new ValueTask(JObject.Parse(token.Properties)); + } + + /// + /// Retrieves the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the reference identifier associated with the specified token. + /// + public virtual ValueTask GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.ReferenceId); + } + + /// + /// Retrieves the status associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the status associated with the specified token. + /// + public virtual ValueTask GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Status); + } + + /// + /// Retrieves the subject associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the subject associated with the specified token. + /// + public virtual ValueTask GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Subject); + } + + /// + /// Retrieves the token type associated with a token. + /// + /// The token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the token type associated with the specified token. + /// + public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + return new ValueTask(token.Type); + } + + /// + /// Instantiates a new token. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the instantiated token, that can be persisted in the database. + /// + public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) + => new ValueTask(new TToken()); + + /// + /// Executes the specified query and returns all the corresponding elements. + /// + /// The number of results to return. + /// The number of results to skip. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns all the elements returned when executing the specified query. + /// + public virtual async Task> ListAsync( + [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) + { + var query = Tokens.Include(token => token.Application) + .Include(token => token.Authorization) + .OrderBy(token => token.Id) + .AsQueryable(); + + if (offset.HasValue) + { + query = query.Skip(offset.Value); + } + + if (count.HasValue) + { + query = query.Take(count.Value); + } + + return ImmutableArray.CreateRange(await query.ToListAsync(cancellationToken)); + } + /// /// Executes the specified query and returns all the corresponding elements. /// @@ -447,7 +689,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns all the elements returned when executing the specified query. /// - public override async Task> ListAsync( + public virtual async Task> ListAsync( [NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken) { @@ -469,7 +711,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task PruneAsync(CancellationToken cancellationToken) + public virtual async Task PruneAsync(CancellationToken cancellationToken) { // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing // entities in a single command without having to retrieve and materialize them first. @@ -477,13 +719,6 @@ namespace OpenIddict.EntityFrameworkCore IList exceptions = null; - IQueryable Query(IQueryable tokens, int offset) - => (from token in tokens.AsTracking() - where token.ExpirationDate < DateTimeOffset.UtcNow || - token.Status != OpenIddictConstants.Statuses.Valid - orderby token.Id - select token).Skip(offset).Take(1_000); - async Task CreateTransactionAsync() { // Note: transactions that specify an explicit isolation level are only supported by @@ -520,8 +755,14 @@ namespace OpenIddict.EntityFrameworkCore // and thus prevent them from being concurrently modified outside this block. using (var transaction = await CreateTransactionAsync()) { - var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); - if (tokens.IsEmpty) + var tokens = + await (from token in Tokens.AsTracking() + where token.ExpirationDate < DateTimeOffset.UtcNow || + token.Status != OpenIddictConstants.Statuses.Valid + orderby token.Id + select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); + + if (tokens.Count == 0) { break; } @@ -561,7 +802,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetApplicationIdAsync([NotNull] TToken token, + public virtual async Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (token == null) @@ -607,7 +848,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override async Task SetAuthorizationIdAsync([NotNull] TToken token, + public virtual async Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) { if (token == null) @@ -644,6 +885,200 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + /// Sets the creation date associated with a token. + /// + /// The token. + /// The creation date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetCreationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.CreationDate = date; + + return Task.CompletedTask; + } + + /// + /// Sets the expiration date associated with a token. + /// + /// The token. + /// The expiration date. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetExpirationDateAsync([NotNull] TToken token, + [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ExpirationDate = date; + + return Task.CompletedTask; + } + + /// + /// Sets the payload associated with a token. + /// + /// The token. + /// The payload associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.Payload = payload; + + return Task.CompletedTask; + } + + /// + /// Sets the additional properties associated with a token. + /// + /// The token. + /// The additional properties associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (properties == null) + { + token.Properties = null; + + return Task.CompletedTask; + } + + token.Properties = properties.ToString(Formatting.None); + + return Task.CompletedTask; + } + + /// + /// Sets the reference identifier associated with a token. + /// Note: depending on the manager used to create the token, + /// the reference identifier may be hashed for security reasons. + /// + /// The token. + /// The reference identifier associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetReferenceIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + token.ReferenceId = identifier; + + return Task.CompletedTask; + } + + /// + /// Sets the status associated with a token. + /// + /// The token. + /// The status associated with the authorization. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetStatusAsync([NotNull] TToken token, [CanBeNull] string status, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + token.Status = status; + + return Task.CompletedTask; + } + + /// + /// Sets the subject associated with a token. + /// + /// The token. + /// The subject associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetSubjectAsync([NotNull] TToken token, [CanBeNull] string subject, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + token.Subject = subject; + + return Task.CompletedTask; + } + + /// + /// Sets the token type associated with a token. + /// + /// The token. + /// The token type associated with the token. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetTokenTypeAsync([NotNull] TToken token, [CanBeNull] string type, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + if (string.IsNullOrEmpty(type)) + { + throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); + } + + token.Type = type; + + return Task.CompletedTask; + } + /// /// Updates an existing token. /// @@ -652,7 +1087,7 @@ namespace OpenIddict.EntityFrameworkCore /// /// A that can be used to monitor the asynchronous operation. /// - public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { @@ -667,7 +1102,48 @@ namespace OpenIddict.EntityFrameworkCore Context.Update(token); - return Context.SaveChangesAsync(cancellationToken); + try + { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException exception) + { + throw new OpenIddictException(OpenIddictConstants.Exceptions.ConcurrencyError, new StringBuilder() + .AppendLine("The token was concurrently updated and cannot be persisted in its current state.") + .Append("Reload the token from the database and retry the operation.") + .ToString(), exception); + } + } + + /// + /// Converts the provided identifier to a strongly typed key object. + /// + /// The identifier to convert. + /// An instance of representing the provided identifier. + public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) + { + if (string.IsNullOrEmpty(identifier)) + { + return default; + } + + return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); + } + + /// + /// Converts the provided identifier to its string representation. + /// + /// The identifier to convert. + /// A representation of the provided identifier. + public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) + { + if (Equals(identifier, default(TKey))) + { + return null; + } + + return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); } } } \ No newline at end of file diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index 6d949cfb..98ef4850 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -391,13 +391,21 @@ namespace OpenIddict.Server return true; } - catch (Exception exception) + catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError) { - _logger.LogDebug(exception, "An exception occurred while trying to revoke the authorization " + + _logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the authorization " + "associated with the token '{Identifier}'.", identifier); return false; } + + catch (Exception exception) + { + _logger.LogWarning(exception, "An exception occurred while trying to revoke the authorization " + + "associated with the token '{Identifier}'.", identifier); + + return false; + } } private async Task TryRevokeTokenAsync([NotNull] object token) @@ -416,10 +424,16 @@ namespace OpenIddict.Server return true; } + catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError) + { + _logger.LogDebug(exception, "A concurrency exception occurred while trying to revoke the token '{Identifier}'.", identifier); + + return false; + } + catch (Exception exception) { - _logger.LogDebug(exception, "An exception occurred while trying to revoke " + - "the token '{Identifier}'.", identifier); + _logger.LogWarning(exception, "An exception occurred while trying to revoke the token '{Identifier}'.", identifier); return false; } @@ -466,10 +480,16 @@ namespace OpenIddict.Server return true; } + catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError) + { + _logger.LogDebug(exception, "A concurrency exception occurred while trying to redeem with the token '{Identifier}'.", identifier); + + return false; + } + catch (Exception exception) { - _logger.LogDebug(exception, "An exception occurred while trying to " + - "redeem the token '{Identifier}'.", identifier); + _logger.LogWarning(exception, "An exception occurred while trying to redeem the token '{Identifier}'.", identifier); return false; } @@ -497,13 +517,21 @@ namespace OpenIddict.Server return true; } - catch (Exception exception) + catch (OpenIddictException exception) when (exception.Reason == OpenIddictConstants.Exceptions.ConcurrencyError) { - _logger.LogDebug(exception, "An exception occurred while trying to update the " + + _logger.LogDebug(exception, "A concurrency exception occurred while trying to update the " + "expiration date of the token '{Identifier}'.", identifier); return false; } + + catch (Exception exception) + { + _logger.LogWarning(exception, "An exception occurred while trying to update the " + + "expiration date of the token '{Identifier}'.", identifier); + + return false; + } } private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters( diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 49b67c56..373b4f1e 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -59,11 +59,6 @@ namespace OpenIddict.Server OpenIdConnectConstants.Claims.Subject }; - /// - /// Gets or sets a boolean indicating whether scope validation is enabled. - /// - public bool EnableScopeValidation { get; set; } - /// /// Gets or sets a boolean indicating whether token revocation should be disabled. /// When disabled, authorization code and refresh tokens are not stored @@ -80,6 +75,11 @@ namespace OpenIddict.Server /// public bool EnableRequestCaching { get; set; } + /// + /// Gets or sets a boolean indicating whether scope validation is enabled. + /// + public bool EnableScopeValidation { get; set; } + /// /// Gets the OAuth2/OpenID Connect flows enabled for this application. /// diff --git a/src/OpenIddict.Stores/OpenIddict.Stores.csproj b/src/OpenIddict.Stores/OpenIddict.Stores.csproj deleted file mode 100644 index b214c47e..00000000 --- a/src/OpenIddict.Stores/OpenIddict.Stores.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - netstandard2.0 - - - - Default base stores for OpenIddict. - Kévin Chalet - aspnetcore;authentication;jwt;openidconnect;openiddict;security - - - - - - - - - - - - - diff --git a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs deleted file mode 100644 index c1cba8bc..00000000 --- a/src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs +++ /dev/null @@ -1,832 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Models; - -namespace OpenIddict.Stores -{ - /// - /// Provides methods allowing to manage the applications stored in a database. - /// Note: this base class can only be used with the default OpenIddict entities. - /// - /// The type of the Application entity. - /// The type of the Authorization entity. - /// The type of the Token entity. - /// The type of the entity primary keys. - public abstract class OpenIddictApplicationStore : IOpenIddictApplicationStore - where TApplication : OpenIddictApplication, new() - where TAuthorization : OpenIddictAuthorization, new() - where TToken : OpenIddictToken, new() - where TKey : IEquatable - { - protected OpenIddictApplicationStore([NotNull] IMemoryCache cache) - { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - - Cache = cache; - } - - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - - /// - /// Determines the number of applications that exist in the database. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of applications in the database. - /// - public virtual Task CountAsync(CancellationToken cancellationToken) - { - return CountAsync(applications => applications, cancellationToken); - } - - /// - /// Determines the number of applications that match the specified query. - /// - /// The result type. - /// The query to execute. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of applications that match the specified query. - /// - public abstract Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken); - - /// - /// Creates a new application. - /// - /// The application to create. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken); - - /// - /// Removes an existing application. - /// - /// The application to delete. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken); - - /// - /// Retrieves an application using its unique identifier. - /// - /// The unique identifier associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client application corresponding to the identifier. - /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable applications, TKey key) - => from application in applications - where application.Id.Equals(key) - select application; - - return GetAsync((applications, key) => Query(applications, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves an application using its client identifier. - /// - /// The client identifier associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client application corresponding to the identifier. - /// - public virtual Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable applications, string id) - => from application in applications - where application.ClientId == id - select application; - - return GetAsync((applications, id) => Query(applications, id), identifier, cancellationToken); - } - - /// - /// Retrieves all the applications associated with the specified post_logout_redirect_uri. - /// - /// The post_logout_redirect_uri associated with the applications. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result - /// returns the client applications corresponding to the specified post_logout_redirect_uri. - /// - public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(address)) - { - throw new ArgumentException("The address cannot be null or empty.", nameof(address)); - } - - // To optimize the efficiency of the query a bit, only applications whose stringified - // PostLogoutRedirectUris contains the specified URL are returned. Once the applications - // are retrieved, a second pass is made to ensure only valid elements are returned. - // Implementers that use this method in a hot path may want to override this method - // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - IQueryable Query(IQueryable applications, string uri) - => from application in applications - where application.PostLogoutRedirectUris.Contains(uri) - select application; - - var builder = ImmutableArray.CreateBuilder(); - - foreach (var application in await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken)) - { - foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) - { - // Note: the post_logout_redirect_uri must be compared - // using case-sensitive "Simple String Comparison". - if (string.Equals(uri, address, StringComparison.Ordinal)) - { - builder.Add(application); - - break; - } - } - } - - return builder.Count == builder.Capacity ? - builder.MoveToImmutable() : - builder.ToImmutable(); - } - - /// - /// Retrieves all the applications associated with the specified redirect_uri. - /// - /// The redirect_uri associated with the applications. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, whose result - /// returns the client applications corresponding to the specified redirect_uri. - /// - public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(address)) - { - throw new ArgumentException("The address cannot be null or empty.", nameof(address)); - } - - // To optimize the efficiency of the query a bit, only applications whose stringified - // RedirectUris property contains the specified URL are returned. Once the applications - // are retrieved, a second pass is made to ensure only valid elements are returned. - // Implementers that use this method in a hot path may want to override this method - // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - IQueryable Query(IQueryable applications, string uri) - => from application in applications - where application.RedirectUris.Contains(uri) - select application; - - var builder = ImmutableArray.CreateBuilder(); - - foreach (var application in await ListAsync((applications, uri) => Query(applications, uri), address, cancellationToken)) - { - foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken)) - { - // Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". - // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. - if (string.Equals(uri, address, StringComparison.Ordinal)) - { - builder.Add(application); - - break; - } - } - } - - return builder.Count == builder.Capacity ? - builder.MoveToImmutable() : - builder.ToImmutable(); - } - - /// - /// Executes the specified query and returns the first element. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the first element returned when executing the query. - /// - public abstract Task GetAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Retrieves the client identifier associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client identifier associated with the application. - /// - public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(application.ClientId); - } - - /// - /// Retrieves the client secret associated with an application. - /// Note: depending on the manager used to create the application, - /// the client secret may be hashed for security reasons. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client secret associated with the application. - /// - public virtual ValueTask GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(application.ClientSecret); - } - - /// - /// Retrieves the client type associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the client type of the application (by default, "public"). - /// - public virtual ValueTask GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(application.Type); - } - - /// - /// Retrieves the consent type associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the consent type of the application (by default, "explicit"). - /// - public virtual ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(application.ConsentType); - } - - /// - /// Retrieves the display name associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the display name associated with the application. - /// - public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(application.DisplayName); - } - - /// - /// Retrieves the unique identifier associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the unique identifier associated with the application. - /// - public virtual ValueTask GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return new ValueTask(ConvertIdentifierToString(application.Id)); - } - - /// - /// Retrieves the permissions associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the permissions associated with the application. - /// - public virtual ValueTask> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (string.IsNullOrEmpty(application.Permissions)) - { - return new ValueTask>(ImmutableArray.Create()); - } - - // Note: parsing the stringified permissions is an expensive operation. - // To mitigate that, the resulting array is stored in the memory cache. - var key = string.Concat("0347e0aa-3a26-410a-97e8-a83bdeb21a1f", "\x1e", application.Permissions); - var permissions = Cache.GetOrCreate(key, entry => - { - entry.SetPriority(CacheItemPriority.High) - .SetSlidingExpiration(TimeSpan.FromMinutes(1)); - - return JArray.Parse(application.Permissions) - .Select(element => (string) element) - .ToImmutableArray(); - }); - - return new ValueTask>(permissions); - } - - /// - /// Retrieves the logout callback addresses associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the post_logout_redirect_uri associated with the application. - /// - public virtual ValueTask> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (string.IsNullOrEmpty(application.PostLogoutRedirectUris)) - { - return new ValueTask>(ImmutableArray.Create()); - } - - // Note: parsing the stringified addresses is an expensive operation. - // To mitigate that, the resulting array is stored in the memory cache. - var key = string.Concat("fb14dfb9-9216-4b77-bfa9-7e85f8201ff4", "\x1e", application.PostLogoutRedirectUris); - var addresses = Cache.GetOrCreate(key, entry => - { - entry.SetPriority(CacheItemPriority.High) - .SetSlidingExpiration(TimeSpan.FromMinutes(1)); - - return JArray.Parse(application.PostLogoutRedirectUris) - .Select(element => (string) element) - .ToImmutableArray(); - }); - - return new ValueTask>(addresses); - } - - /// - /// Retrieves the additional properties associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the additional properties associated with the application. - /// - public virtual ValueTask GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (string.IsNullOrEmpty(application.Properties)) - { - return new ValueTask(new JObject()); - } - - return new ValueTask(JObject.Parse(application.Properties)); - } - - /// - /// Retrieves the callback addresses associated with an application. - /// - /// The application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the redirect_uri associated with the application. - /// - public virtual ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (string.IsNullOrEmpty(application.RedirectUris)) - { - return new ValueTask>(ImmutableArray.Create()); - } - - // Note: parsing the stringified addresses is an expensive operation. - // To mitigate that, the resulting array is stored in the memory cache. - var key = string.Concat("851d6f08-2ee0-4452-bbe5-ab864611ecaa", "\x1e", application.RedirectUris); - var addresses = Cache.GetOrCreate(key, entry => - { - entry.SetPriority(CacheItemPriority.High) - .SetSlidingExpiration(TimeSpan.FromMinutes(1)); - - return JArray.Parse(application.RedirectUris) - .Select(element => (string) element) - .ToImmutableArray(); - }); - - return new ValueTask>(addresses); - } - - /// - /// Instantiates a new application. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the instantiated application, that can be persisted in the database. - /// - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) - => new ValueTask(new TApplication()); - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The number of results to return. - /// The number of results to skip. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public virtual Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) - { - IQueryable Query(IQueryable applications, int? skip, int? take) - { - var query = applications.OrderBy(application => application.Id).AsQueryable(); - - if (skip.HasValue) - { - query = query.Skip(skip.Value); - } - - if (take.HasValue) - { - query = query.Take(take.Value); - } - - return query; - } - - return ListAsync((applications, state) => Query(applications, state.offset, state.count), (offset, count), cancellationToken); - } - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public abstract Task> ListAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Sets the client identifier associated with an application. - /// - /// The application. - /// The client identifier associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetClientIdAsync([NotNull] TApplication application, - [CanBeNull] string identifier, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - application.ClientId = identifier; - - return Task.CompletedTask; - } - - /// - /// Sets the client secret associated with an application. - /// Note: depending on the manager used to create the application, - /// the client secret may be hashed for security reasons. - /// - /// The application. - /// The client secret associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetClientSecretAsync([NotNull] TApplication application, - [CanBeNull] string secret, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - application.ClientSecret = secret; - - return Task.CompletedTask; - } - - /// - /// Sets the client type associated with an application. - /// - /// The application. - /// The client type associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetClientTypeAsync([NotNull] TApplication application, - [CanBeNull] string type, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - application.Type = type; - - return Task.CompletedTask; - } - - /// - /// Sets the consent type associated with an application. - /// - /// The application. - /// The consent type associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetConsentTypeAsync([NotNull] TApplication application, - [CanBeNull] string type, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - application.ConsentType = type; - - return Task.CompletedTask; - } - - /// - /// Sets the display name associated with an application. - /// - /// The application. - /// The display name associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetDisplayNameAsync([NotNull] TApplication application, - [CanBeNull] string name, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - application.DisplayName = name; - - return Task.CompletedTask; - } - - /// - /// Sets the permissions associated with an application. - /// - /// The application. - /// The permissions associated with the application - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray permissions, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (permissions.IsDefaultOrEmpty) - { - application.Permissions = null; - - return Task.CompletedTask; - } - - application.Permissions = new JArray(permissions.ToArray()).ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the logout callback addresses associated with an application. - /// - /// The application. - /// The logout callback addresses associated with the application - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application, - ImmutableArray addresses, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (addresses.IsDefaultOrEmpty) - { - application.PostLogoutRedirectUris = null; - - return Task.CompletedTask; - } - - application.PostLogoutRedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the additional properties associated with an application. - /// - /// The application. - /// The additional properties associated with the application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPropertiesAsync([NotNull] TApplication application, [CanBeNull] JObject properties, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (properties == null) - { - application.Properties = null; - - return Task.CompletedTask; - } - - application.Properties = properties.ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the callback addresses associated with an application. - /// - /// The application. - /// The callback addresses associated with the application - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetRedirectUrisAsync([NotNull] TApplication application, - ImmutableArray addresses, CancellationToken cancellationToken) - { - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (addresses.IsDefaultOrEmpty) - { - application.RedirectUris = null; - - return Task.CompletedTask; - } - - application.RedirectUris = new JArray(addresses.ToArray()).ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Updates an existing application. - /// - /// The application to update. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken); - - /// - /// Converts the provided identifier to a strongly typed key object. - /// - /// The identifier to convert. - /// An instance of representing the provided identifier. - public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) - { - if (string.IsNullOrEmpty(identifier)) - { - return default; - } - - return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); - } - - /// - /// Converts the provided identifier to its string representation. - /// - /// The identifier to convert. - /// A representation of the provided identifier. - public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) - { - if (Equals(identifier, default(TKey))) - { - return null; - } - - return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs deleted file mode 100644 index aaaa0e21..00000000 --- a/src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs +++ /dev/null @@ -1,694 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Models; - -namespace OpenIddict.Stores -{ - /// - /// Provides methods allowing to manage the authorizations stored in a database. - /// Note: this base class can only be used with the default OpenIddict entities. - /// - /// The type of the Authorization entity. - /// The type of the Application entity. - /// The type of the Token entity. - /// The type of the entity primary keys. - public abstract class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore - where TAuthorization : OpenIddictAuthorization, new() - where TApplication : OpenIddictApplication, new() - where TToken : OpenIddictToken, new() - where TKey : IEquatable - { - protected OpenIddictAuthorizationStore([NotNull] IMemoryCache cache) - { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - - Cache = cache; - } - - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - - /// - /// Determines the number of authorizations that exist in the database. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of authorizations in the database. - /// - public virtual Task CountAsync(CancellationToken cancellationToken) - { - return CountAsync(authorizations => authorizations, cancellationToken); - } - - /// - /// Determines the number of authorizations that match the specified query. - /// - /// The result type. - /// The query to execute. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of authorizations that match the specified query. - /// - public abstract Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken); - - /// - /// Creates a new authorization. - /// - /// The authorization to create. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); - - /// - /// Removes an existing authorization. - /// - /// The authorization to delete. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); - - /// - /// Retrieves the authorizations corresponding to the specified - /// subject and associated with the application identifier. - /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorizations corresponding to the subject/client. - /// - public virtual Task> FindAsync( - [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - if (string.IsNullOrEmpty(client)) - { - throw new ArgumentException("The client cannot be null or empty.", nameof(client)); - } - - IQueryable Query(IQueryable authorizations, TKey key, string principal) - => from authorization in authorizations - where authorization.Application != null && - authorization.Application.Id.Equals(key) && - authorization.Subject == principal - select authorization; - - return ListAsync( - (authorizations, state) => Query(authorizations, state.key, state.principal), - (key: ConvertIdentifierFromString(client), principal: subject), cancellationToken); - } - - /// - /// Retrieves the authorizations matching the specified parameters. - /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization status. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorizations corresponding to the criteria. - /// - public virtual Task> FindAsync( - [NotNull] string subject, [NotNull] string client, - [NotNull] string status, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - if (string.IsNullOrEmpty(client)) - { - throw new ArgumentException("The client cannot be null or empty.", nameof(client)); - } - - if (string.IsNullOrEmpty(status)) - { - throw new ArgumentException("The status cannot be null or empty.", nameof(status)); - } - - IQueryable Query(IQueryable authorizations, TKey key, string principal, string state) - => from authorization in authorizations - where authorization.Application != null && - authorization.Application.Id.Equals(key) && - authorization.Subject == principal && - authorization.Status == state - select authorization; - - return ListAsync( - (authorizations, state) => Query(authorizations, state.key, state.principal, state.state), - (key: ConvertIdentifierFromString(client), principal: subject, state: status), cancellationToken); - } - - /// - /// Retrieves the authorizations matching the specified parameters. - /// - /// The subject associated with the authorization. - /// The client associated with the authorization. - /// The authorization status. - /// The authorization type. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorizations corresponding to the criteria. - /// - public virtual Task> FindAsync( - [NotNull] string subject, [NotNull] string client, - [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - if (string.IsNullOrEmpty(client)) - { - throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); - } - - if (string.IsNullOrEmpty(status)) - { - throw new ArgumentException("The status cannot be null or empty.", nameof(status)); - } - - if (string.IsNullOrEmpty(type)) - { - throw new ArgumentException("The type cannot be null or empty.", nameof(type)); - } - - IQueryable Query(IQueryable authorizations, - TKey key, string principal, string state, string kind) - => from authorization in authorizations - where authorization.Application != null && - authorization.Application.Id.Equals(key) && - authorization.Subject == principal && - authorization.Status == state && - authorization.Type == kind - select authorization; - - return ListAsync( - (authorizations, state) => Query(authorizations, state.key, state.principal, state.state, state.kind), - (key: ConvertIdentifierFromString(client), principal: subject, state: status, kind: type), cancellationToken); - } - - /// - /// Retrieves an authorization using its unique identifier. - /// - /// The unique identifier associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorization corresponding to the identifier. - /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable authorizations, TKey key) - => from authorization in authorizations - where authorization.Id.Equals(key) - select authorization; - - return GetAsync((authorizations, key) => Query(authorizations, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves all the authorizations corresponding to the specified subject. - /// - /// The subject associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorizations corresponding to the specified subject. - /// - public virtual Task> FindBySubjectAsync( - [NotNull] string subject, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - IQueryable Query(IQueryable authorizations, string principal) - => from authorization in authorizations - where authorization.Subject == principal - select authorization; - - return ListAsync((authorizations, principal) => Query(authorizations, principal), subject, cancellationToken); - } - - /// - /// Retrieves the optional application identifier associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the application identifier associated with the authorization. - /// - public virtual ValueTask GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - if (authorization.Application != null) - { - return new ValueTask(ConvertIdentifierToString(authorization.Application.Id)); - } - - async Task RetrieveApplicationIdAsync() - { - IQueryable Query(IQueryable authorizations, TKey key) - => from element in authorizations - where element.Id.Equals(key) && - element.Application != null - select element.Application.Id; - - return ConvertIdentifierToString(await GetAsync( - (authorizations, key) => Query(authorizations, key), authorization.Id, cancellationToken)); - } - - return new ValueTask(RetrieveApplicationIdAsync()); - } - - /// - /// Executes the specified query and returns the first element. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the first element returned when executing the query. - /// - public abstract Task GetAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Retrieves the unique identifier associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the unique identifier associated with the authorization. - /// - public virtual ValueTask GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - return new ValueTask(ConvertIdentifierToString(authorization.Id)); - } - - /// - /// Retrieves the additional properties associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the additional properties associated with the authorization. - /// - public virtual ValueTask GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - if (string.IsNullOrEmpty(authorization.Properties)) - { - return new ValueTask(new JObject()); - } - - return new ValueTask(JObject.Parse(authorization.Properties)); - } - - /// - /// Retrieves the scopes associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the scopes associated with the specified authorization. - /// - public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - if (string.IsNullOrEmpty(authorization.Scopes)) - { - return new ValueTask>(ImmutableArray.Create()); - } - - return new ValueTask>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); - } - - /// - /// Retrieves the status associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the status associated with the specified authorization. - /// - public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - return new ValueTask(authorization.Status); - } - - /// - /// Retrieves the subject associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the subject associated with the specified authorization. - /// - public virtual ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - return new ValueTask(authorization.Subject); - } - - /// - /// Retrieves the type associated with an authorization. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the type associated with the specified authorization. - /// - public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - return new ValueTask(authorization.Type); - } - - /// - /// Instantiates a new authorization. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the instantiated authorization, that can be persisted in the database. - /// - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) - => new ValueTask(new TAuthorization()); - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The number of results to return. - /// The number of results to skip. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public virtual Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) - { - IQueryable Query(IQueryable authorizations, int? skip, int? take) - { - var query = authorizations.OrderBy(authorization => authorization.Id).AsQueryable(); - - if (skip.HasValue) - { - query = query.Skip(skip.Value); - } - - if (take.HasValue) - { - query = query.Take(take.Value); - } - - return query; - } - - return ListAsync((authorizations, state) => Query(authorizations, state.offset, state.count), (offset, count), cancellationToken); - } - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public abstract Task> ListAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task PruneAsync(CancellationToken cancellationToken); - - /// - /// Sets the application identifier associated with an authorization. - /// - /// The authorization. - /// The unique identifier associated with the client application. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task SetApplicationIdAsync([NotNull] TAuthorization authorization, - [CanBeNull] string identifier, CancellationToken cancellationToken); - - /// - /// Sets the additional properties associated with an authorization. - /// - /// The authorization. - /// The additional properties associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPropertiesAsync([NotNull] TAuthorization authorization, [CanBeNull] JObject properties, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - if (properties == null) - { - authorization.Properties = null; - - return Task.CompletedTask; - } - - authorization.Properties = properties.ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the scopes associated with an authorization. - /// - /// The authorization. - /// The scopes associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetScopesAsync([NotNull] TAuthorization authorization, - ImmutableArray scopes, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - if (scopes.IsDefaultOrEmpty) - { - authorization.Scopes = null; - - return Task.CompletedTask; - } - - authorization.Scopes = new JArray(scopes.ToArray()).ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the status associated with an authorization. - /// - /// The authorization. - /// The status associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetStatusAsync([NotNull] TAuthorization authorization, - [CanBeNull] string status, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - authorization.Status = status; - - return Task.CompletedTask; - } - - /// - /// Sets the subject associated with an authorization. - /// - /// The authorization. - /// The subject associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetSubjectAsync([NotNull] TAuthorization authorization, - [CanBeNull] string subject, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - authorization.Subject = subject; - - return Task.CompletedTask; - } - - /// - /// Sets the type associated with an authorization. - /// - /// The authorization. - /// The type associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetTypeAsync([NotNull] TAuthorization authorization, - [CanBeNull] string type, CancellationToken cancellationToken) - { - if (authorization == null) - { - throw new ArgumentNullException(nameof(authorization)); - } - - authorization.Type = type; - - return Task.CompletedTask; - } - - /// - /// Updates an existing authorization. - /// - /// The authorization to update. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); - - /// - /// Converts the provided identifier to a strongly typed key object. - /// - /// The identifier to convert. - /// An instance of representing the provided identifier. - public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) - { - if (string.IsNullOrEmpty(identifier)) - { - return default; - } - - return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); - } - - /// - /// Converts the provided identifier to its string representation. - /// - /// The identifier to convert. - /// A representation of the provided identifier. - public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) - { - if (Equals(identifier, default(TKey))) - { - return null; - } - - return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs deleted file mode 100644 index cdfd2d27..00000000 --- a/src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs +++ /dev/null @@ -1,577 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Models; - -namespace OpenIddict.Stores -{ - /// - /// Provides methods allowing to manage the scopes stored in a database. - /// Note: this base class can only be used with the default OpenIddict entities. - /// - /// The type of the Scope entity. - /// The type of the entity primary keys. - public abstract class OpenIddictScopeStore : IOpenIddictScopeStore - where TScope : OpenIddictScope, new() - where TKey : IEquatable - { - protected OpenIddictScopeStore([NotNull] IMemoryCache cache) - { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - - Cache = cache; - } - - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - - /// - /// Determines the number of scopes that exist in the database. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of scopes in the database. - /// - public virtual Task CountAsync(CancellationToken cancellationToken) - { - return CountAsync(scopes => scopes, cancellationToken); - } - - /// - /// Determines the number of scopes that match the specified query. - /// - /// The result type. - /// The query to execute. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of scopes that match the specified query. - /// - public abstract Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken); - - /// - /// Creates a new scope. - /// - /// The scope to create. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken); - - /// - /// Removes an existing scope. - /// - /// The scope to delete. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken); - - /// - /// Retrieves a scope using its unique identifier. - /// - /// The unique identifier associated with the scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the scope corresponding to the identifier. - /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable scopes, TKey key) - => from scope in scopes - where scope.Id.Equals(key) - select scope; - - return GetAsync((scopes, key) => Query(scopes, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves a scope using its name. - /// - /// The name associated with the scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the scope corresponding to the specified name. - /// - public virtual Task FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); - } - - IQueryable Query(IQueryable scopes, string state) - => from scope in scopes - where scope.Name == state - select scope; - - return GetAsync((scopes, state) => Query(scopes, state), name, cancellationToken); - } - - /// - /// Retrieves a list of scopes using their name. - /// - /// The names associated with the scopes. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the scopes corresponding to the specified names. - /// - public virtual Task> FindByNamesAsync( - ImmutableArray names, CancellationToken cancellationToken) - { - if (names.Any(name => string.IsNullOrEmpty(name))) - { - throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); - } - - IQueryable Query(IQueryable scopes, string[] values) - => from scope in scopes - where values.Contains(scope.Name) - select scope; - - return ListAsync((scopes, values) => Query(scopes, values), names.ToArray(), cancellationToken); - } - - /// - /// Retrieves all the scopes that contain the specified resource. - /// - /// The resource associated with the scopes. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the scopes associated with the specified resource. - /// - public virtual async Task> FindByResourceAsync( - [NotNull] string resource, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(resource)) - { - throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); - } - - // To optimize the efficiency of the query a bit, only scopes whose stringified - // Resources column contains the specified resource are returned. Once the scopes - // are retrieved, a second pass is made to ensure only valid elements are returned. - // Implementers that use this method in a hot path may want to override this method - // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - IQueryable Query(IQueryable scopes, string state) - => from scope in scopes - where scope.Resources.Contains(state) - select scope; - - var builder = ImmutableArray.CreateBuilder(); - - foreach (var application in await ListAsync((applications, state) => Query(applications, state), resource, cancellationToken)) - { - var resources = await GetResourcesAsync(application, cancellationToken); - if (resources.Contains(resource, StringComparer.OrdinalIgnoreCase)) - { - builder.Add(application); - } - } - - return builder.ToImmutable(); - } - - /// - /// Executes the specified query and returns the first element. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the first element returned when executing the query. - /// - public abstract Task GetAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Retrieves the description associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the description associated with the specified scope. - /// - public virtual ValueTask GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - return new ValueTask(scope.Description); - } - - /// - /// Retrieves the display name associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the display name associated with the scope. - /// - public virtual ValueTask GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - return new ValueTask(scope.DisplayName); - } - - /// - /// Retrieves the unique identifier associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the unique identifier associated with the scope. - /// - public virtual ValueTask GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - return new ValueTask(ConvertIdentifierToString(scope.Id)); - } - - /// - /// Retrieves the name associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the name associated with the specified scope. - /// - public virtual ValueTask GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - return new ValueTask(scope.Name); - } - - /// - /// Retrieves the additional properties associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the additional properties associated with the scope. - /// - public virtual ValueTask GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - if (string.IsNullOrEmpty(scope.Properties)) - { - return new ValueTask(new JObject()); - } - - return new ValueTask(JObject.Parse(scope.Properties)); - } - - /// - /// Retrieves the resources associated with a scope. - /// - /// The scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the resources associated with the scope. - /// - public virtual ValueTask> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - if (string.IsNullOrEmpty(scope.Resources)) - { - return new ValueTask>(ImmutableArray.Create()); - } - - // Note: parsing the stringified resources is an expensive operation. - // To mitigate that, the resulting array is stored in the memory cache. - var key = string.Concat("b6148250-aede-4fb9-a621-07c9bcf238c3", "\x1e", scope.Resources); - var resources = Cache.GetOrCreate(key, entry => - { - entry.SetPriority(CacheItemPriority.High) - .SetSlidingExpiration(TimeSpan.FromMinutes(1)); - - return JArray.Parse(scope.Resources) - .Select(element => (string) element) - .ToImmutableArray(); - }); - - return new ValueTask>(resources); - } - - /// - /// Instantiates a new scope. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the instantiated scope, that can be persisted in the database. - /// - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) - => new ValueTask(new TScope()); - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The number of results to return. - /// The number of results to skip. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public virtual Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) - { - IQueryable Query(IQueryable scopes, int? skip, int? take) - { - var query = scopes.OrderBy(scope => scope.Id).AsQueryable(); - - if (skip.HasValue) - { - query = query.Skip(skip.Value); - } - - if (take.HasValue) - { - query = query.Take(take.Value); - } - - return query; - } - - return ListAsync((scopes, state) => Query(scopes, state.offset, state.count), (offset, count), cancellationToken); - } - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public abstract Task> ListAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Sets the description associated with a scope. - /// - /// The scope. - /// The description associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetDescriptionAsync([NotNull] TScope scope, [CanBeNull] string description, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - scope.Description = description; - - return Task.CompletedTask; - } - - /// - /// Sets the display name associated with a scope. - /// - /// The scope. - /// The display name associated with the scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetDisplayNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - scope.DisplayName = name; - - return Task.CompletedTask; - } - - /// - /// Sets the name associated with a scope. - /// - /// The scope. - /// The name associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetNameAsync([NotNull] TScope scope, [CanBeNull] string name, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - scope.Name = name; - - return Task.CompletedTask; - } - - /// - /// Sets the additional properties associated with a scope. - /// - /// The scope. - /// The additional properties associated with the scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPropertiesAsync([NotNull] TScope scope, [CanBeNull] JObject properties, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - if (properties == null) - { - scope.Properties = null; - - return Task.CompletedTask; - } - - scope.Properties = properties.ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the resources associated with a scope. - /// - /// The scope. - /// The resources associated with the scope. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray resources, CancellationToken cancellationToken) - { - if (scope == null) - { - throw new ArgumentNullException(nameof(scope)); - } - - if (resources.IsDefaultOrEmpty) - { - scope.Resources = null; - - return Task.CompletedTask; - } - - scope.Resources = new JArray(resources.ToArray()).ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Updates an existing scope. - /// - /// The scope to update. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken); - - /// - /// Converts the provided identifier to a strongly typed key object. - /// - /// The identifier to convert. - /// An instance of representing the provided identifier. - public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) - { - if (string.IsNullOrEmpty(identifier)) - { - return default; - } - - return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); - } - - /// - /// Converts the provided identifier to its string representation. - /// - /// The identifier to convert. - /// A representation of the provided identifier. - public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) - { - if (Equals(identifier, default(TKey))) - { - return null; - } - - return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs deleted file mode 100644 index f396118c..00000000 --- a/src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs +++ /dev/null @@ -1,805 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Collections.Immutable; -using System.ComponentModel; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OpenIddict.Abstractions; -using OpenIddict.Models; - -namespace OpenIddict.Stores -{ - /// - /// Provides methods allowing to manage the tokens stored in a database. - /// Note: this base class can only be used with the default OpenIddict entities. - /// - /// The type of the Token entity. - /// The type of the Application entity. - /// The type of the Authorization entity. - /// The type of the entity primary keys. - public abstract class OpenIddictTokenStore : IOpenIddictTokenStore - where TToken : OpenIddictToken, new() - where TApplication : OpenIddictApplication, new() - where TAuthorization : OpenIddictAuthorization, new() - where TKey : IEquatable - { - protected OpenIddictTokenStore([NotNull] IMemoryCache cache) - { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - - Cache = cache; - } - - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - - /// - /// Determines the number of tokens that exist in the database. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of applications in the database. - /// - public virtual Task CountAsync(CancellationToken cancellationToken) - { - return CountAsync(tokens => tokens, cancellationToken); - } - - /// - /// Determines the number of tokens that match the specified query. - /// - /// The result type. - /// The query to execute. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the number of tokens that match the specified query. - /// - public abstract Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken); - - /// - /// Creates a new token. - /// - /// The token to create. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken); - - /// - /// Removes a token. - /// - /// The token to delete. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - public abstract Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken); - - /// - /// Retrieves the list of tokens corresponding to the specified application identifier. - /// - /// The application identifier associated with the tokens. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the tokens corresponding to the specified application. - /// - public virtual Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable tokens, TKey key) - => from token in tokens - where token.Application != null && - token.Application.Id.Equals(key) - select token; - - return ListAsync((tokens, key) => Query(tokens, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves the list of tokens corresponding to the specified authorization identifier. - /// - /// The authorization identifier associated with the tokens. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the tokens corresponding to the specified authorization. - /// - public virtual Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable tokens, TKey key) - => from token in tokens - where token.Authorization != null && - token.Authorization.Id.Equals(key) - select token; - - return ListAsync((tokens, key) => Query(tokens, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves a token using its unique identifier. - /// - /// The unique identifier associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the token corresponding to the unique identifier. - /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable tokens, TKey key) - => from token in tokens - where token.Id.Equals(key) - select token; - - return GetAsync((tokens, key) => Query(tokens, key), ConvertIdentifierFromString(identifier), cancellationToken); - } - - /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. - /// - /// The reference identifier associated with the tokens. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the tokens corresponding to the specified reference identifier. - /// - public virtual Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - IQueryable Query(IQueryable tokens, string id) - => from token in tokens - where token.ReferenceId == id - select token; - - return GetAsync((tokens, id) => Query(tokens, identifier), identifier, cancellationToken); - } - - /// - /// Retrieves the list of tokens corresponding to the specified subject. - /// - /// The subject associated with the tokens. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the tokens corresponding to the specified subject. - /// - public virtual Task> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - IQueryable Query(IQueryable tokens, string principal) - => from token in tokens - where token.Subject == principal - select token; - - return ListAsync((tokens, principal) => Query(tokens, principal), subject, cancellationToken); - } - - /// - /// Executes the specified query and returns the first element. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the first element returned when executing the query. - /// - public abstract Task GetAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Retrieves the optional application identifier associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the application identifier associated with the token. - /// - public virtual ValueTask GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (token.Application != null) - { - return new ValueTask(ConvertIdentifierToString(token.Application.Id)); - } - - async Task RetrieveApplicationIdAsync() - { - IQueryable Query(IQueryable tokens, TKey key) - => from element in tokens - where element.Id.Equals(key) && - element.Application != null - select element.Application.Id; - - return ConvertIdentifierToString(await GetAsync((tokens, key) => Query(tokens, key), token.Id, cancellationToken)); - } - - return new ValueTask(RetrieveApplicationIdAsync()); - } - - /// - /// Retrieves the optional authorization identifier associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the authorization identifier associated with the token. - /// - public virtual ValueTask GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (token.Authorization != null) - { - return new ValueTask(ConvertIdentifierToString(token.Authorization.Id)); - } - - async Task RetrieveAuthorizationIdAsync() - { - IQueryable Query(IQueryable tokens, TKey key) - => from element in tokens - where element.Id.Equals(key) && - element.Authorization != null - select element.Authorization.Id; - - return ConvertIdentifierToString(await GetAsync((tokens, key) => Query(tokens, key), token.Id, cancellationToken)); - } - - return new ValueTask(RetrieveAuthorizationIdAsync()); - } - - /// - /// Retrieves the creation date associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the creation date associated with the specified token. - /// - public virtual ValueTask GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.CreationDate); - } - - /// - /// Retrieves the expiration date associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the expiration date associated with the specified token. - /// - public virtual ValueTask GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.ExpirationDate); - } - - /// - /// Retrieves the unique identifier associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the unique identifier associated with the token. - /// - public virtual ValueTask GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(ConvertIdentifierToString(token.Id)); - } - - /// - /// Retrieves the payload associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the payload associated with the specified token. - /// - public virtual ValueTask GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.Payload); - } - - /// - /// Retrieves the additional properties associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the additional properties associated with the token. - /// - public virtual ValueTask GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (string.IsNullOrEmpty(token.Properties)) - { - return new ValueTask(new JObject()); - } - - return new ValueTask(JObject.Parse(token.Properties)); - } - - /// - /// Retrieves the reference identifier associated with a token. - /// Note: depending on the manager used to create the token, - /// the reference identifier may be hashed for security reasons. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the reference identifier associated with the specified token. - /// - public virtual ValueTask GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.ReferenceId); - } - - /// - /// Retrieves the status associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the status associated with the specified token. - /// - public virtual ValueTask GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.Status); - } - - /// - /// Retrieves the subject associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the subject associated with the specified token. - /// - public virtual ValueTask GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.Subject); - } - - /// - /// Retrieves the token type associated with a token. - /// - /// The token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the token type associated with the specified token. - /// - public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - return new ValueTask(token.Type); - } - - /// - /// Instantiates a new token. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns the instantiated token, that can be persisted in the database. - /// - public virtual ValueTask InstantiateAsync(CancellationToken cancellationToken) - => new ValueTask(new TToken()); - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The number of results to return. - /// The number of results to skip. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public virtual Task> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) - { - IQueryable Query(IQueryable tokens, int? skip, int? take) - { - var query = tokens.OrderBy(token => token.Id).AsQueryable(); - - if (skip.HasValue) - { - query = query.Skip(skip.Value); - } - - if (take.HasValue) - { - query = query.Take(take.Value); - } - - return query; - } - - return ListAsync((tokens, state) => Query(tokens, state.offset, state.count), (offset, count), cancellationToken); - } - - /// - /// Executes the specified query and returns all the corresponding elements. - /// - /// The state type. - /// The result type. - /// The query to execute. - /// The optional state. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation, - /// whose result returns all the elements returned when executing the specified query. - /// - public abstract Task> ListAsync( - [NotNull] Func, TState, IQueryable> query, - [CanBeNull] TState state, CancellationToken cancellationToken); - - /// - /// Removes the tokens that are marked as expired or invalid. - /// - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task PruneAsync(CancellationToken cancellationToken); - - /// - /// Sets the authorization identifier associated with a token. - /// - /// The token. - /// The unique identifier associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken); - - /// - /// Sets the application identifier associated with a token. - /// - /// The token. - /// The unique identifier associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken); - - /// - /// Sets the creation date associated with a token. - /// - /// The token. - /// The creation date. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetCreationDateAsync([NotNull] TToken token, - [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - token.CreationDate = date; - - return Task.CompletedTask; - } - - /// - /// Sets the expiration date associated with a token. - /// - /// The token. - /// The expiration date. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetExpirationDateAsync([NotNull] TToken token, - [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - token.ExpirationDate = date; - - return Task.CompletedTask; - } - - /// - /// Sets the payload associated with a token. - /// - /// The token. - /// The payload associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPayloadAsync([NotNull] TToken token, [CanBeNull] string payload, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - token.Payload = payload; - - return Task.CompletedTask; - } - - /// - /// Sets the additional properties associated with a token. - /// - /// The token. - /// The additional properties associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetPropertiesAsync([NotNull] TToken token, [CanBeNull] JObject properties, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (properties == null) - { - token.Properties = null; - - return Task.CompletedTask; - } - - token.Properties = properties.ToString(Formatting.None); - - return Task.CompletedTask; - } - - /// - /// Sets the reference identifier associated with a token. - /// Note: depending on the manager used to create the token, - /// the reference identifier may be hashed for security reasons. - /// - /// The token. - /// The reference identifier associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetReferenceIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - token.ReferenceId = identifier; - - return Task.CompletedTask; - } - - /// - /// Sets the status associated with a token. - /// - /// The token. - /// The status associated with the authorization. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetStatusAsync([NotNull] TToken token, [CanBeNull] string status, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (string.IsNullOrEmpty(status)) - { - throw new ArgumentException("The status cannot be null or empty.", nameof(status)); - } - - token.Status = status; - - return Task.CompletedTask; - } - - /// - /// Sets the subject associated with a token. - /// - /// The token. - /// The subject associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetSubjectAsync([NotNull] TToken token, [CanBeNull] string subject, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (string.IsNullOrEmpty(subject)) - { - throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); - } - - token.Subject = subject; - - return Task.CompletedTask; - } - - /// - /// Sets the token type associated with a token. - /// - /// The token. - /// The token type associated with the token. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task SetTokenTypeAsync([NotNull] TToken token, [CanBeNull] string type, CancellationToken cancellationToken) - { - if (token == null) - { - throw new ArgumentNullException(nameof(token)); - } - - if (string.IsNullOrEmpty(type)) - { - throw new ArgumentException("The token type cannot be null or empty.", nameof(type)); - } - - token.Type = type; - - return Task.CompletedTask; - } - - /// - /// Updates an existing token. - /// - /// The token to update. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public abstract Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken); - - /// - /// Converts the provided identifier to a strongly typed key object. - /// - /// The identifier to convert. - /// An instance of representing the provided identifier. - public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier) - { - if (string.IsNullOrEmpty(identifier)) - { - return default; - } - - return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier); - } - - /// - /// Converts the provided identifier to its string representation. - /// - /// The identifier to convert. - /// A representation of the provided identifier. - public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier) - { - if (Equals(identifier, default(TKey))) - { - return null; - } - - return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier); - } - } -} \ No newline at end of file diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index 19b1a3f6..af978351 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -14,13 +14,8 @@ - - - - - diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs deleted file mode 100644 index d7ea853a..00000000 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using JetBrains.Annotations; -using OpenIddict.Models; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class OpenIddictExtensions - { - /// - /// Configures OpenIddict to use the default entities, with the default entity key type (string). - /// The default entities are , , - /// and . - /// - /// The services builder used by OpenIddict to register new services - /// The . - public static OpenIddictCoreBuilder UseDefaultModels([NotNull] this OpenIddictCoreBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - return builder.UseCustomModels(); - } - - /// - /// Configures OpenIddict to use the default entities, with the specified entity key type. - /// The default entities are , , - /// and . - /// - /// The type of the entity primary keys. - /// The services builder used by OpenIddict to register new services - /// The . - public static OpenIddictCoreBuilder UseDefaultModels([NotNull] this OpenIddictCoreBuilder builder) - where TKey : IEquatable - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - return builder.UseCustomModels, - OpenIddictAuthorization, - OpenIddictScope, - OpenIddictToken>(); - } - } -} diff --git a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs index d72f4560..73d7cce6 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs @@ -356,15 +356,19 @@ namespace OpenIddict.Core.Tests } [Fact] - public void UseCustomModels_CustomEntitiesAreCorrectlySet() + public void RegisterDefaultModels_CustomEntitiesAreCorrectlySet() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - services.AddOpenIddict().AddCore() - .UseCustomModels(); + services.AddOpenIddict() + .AddCore() + .SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); // Assert var provider = services.BuildServiceProvider(); diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs new file mode 100644 index 00000000..25a30425 --- /dev/null +++ b/test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs @@ -0,0 +1,57 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using OpenIddict.Core; +using OpenIddict.EntityFramework; +using OpenIddict.EntityFramework.Models; +using Xunit; + +namespace OpenIddict.EntityFramework.Tests +{ + public class OpenIddictEntityFrameworkExtensionsTests + { + [Theory] + [InlineData(typeof(OpenIddictApplicationStoreResolver))] + [InlineData(typeof(OpenIddictAuthorizationStoreResolver))] + [InlineData(typeof(OpenIddictScopeStoreResolver))] + [InlineData(typeof(OpenIddictTokenStoreResolver))] + public void AddEntityFrameworkStores_RegistersEntityFrameworkStoreFactories(Type type) + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddOpenIddict().AddCore(); + + // Act + builder.UseEntityFramework(); + + // Assert + Assert.Contains(services, service => service.ImplementationType == type); + } + + [Fact] + public void UseEntityFrameworkModels_KeyTypeDefaultsToString() + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddOpenIddict().AddCore(); + + // Act + builder.UseEntityFramework(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(typeof(OpenIddictApplication), options.DefaultApplicationType); + Assert.Equal(typeof(OpenIddictAuthorization), options.DefaultAuthorizationType); + Assert.Equal(typeof(OpenIddictScope), options.DefaultScopeType); + Assert.Equal(typeof(OpenIddictToken), options.DefaultTokenType); + } + } +} diff --git a/test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs deleted file mode 100644 index a0521f3e..00000000 --- a/test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Data.Entity; -using Microsoft.Extensions.DependencyInjection; -using OpenIddict.EntityFramework; -using Xunit; - -namespace OpenIddict.EntityFrameworkCore.Tests -{ - public class OpenIddictExtensionsTests - { - [Theory] - [InlineData(typeof(OpenIddictApplicationStoreResolver))] - [InlineData(typeof(OpenIddictAuthorizationStoreResolver))] - [InlineData(typeof(OpenIddictScopeStoreResolver))] - [InlineData(typeof(OpenIddictTokenStoreResolver))] - public void AddEntityFrameworkStores_RegistersEntityFrameworkStoreFactories(Type type) - { - // Arrange - var services = new ServiceCollection(); - var builder = services.AddOpenIddict().AddCore(); - - // Act - builder.AddEntityFrameworkStores(); - - // Assert - Assert.Contains(services, service => service.ImplementationType == type); - } - } -} diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs similarity index 62% rename from test/OpenIddict.Tests/OpenIddictExtensionsTests.cs rename to test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs index da94f23f..28fd1bad 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs @@ -8,22 +8,40 @@ using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using OpenIddict.Core; -using OpenIddict.Models; +using OpenIddict.EntityFrameworkCore.Models; using Xunit; -namespace OpenIddict.Tests +namespace OpenIddict.EntityFrameworkCore.Tests { - public class OpenIddictExtensionsTests + public class OpenIddictEntityFrameworkCoreExtensionsTests { + [Theory] + [InlineData(typeof(OpenIddictApplicationStoreResolver))] + [InlineData(typeof(OpenIddictAuthorizationStoreResolver))] + [InlineData(typeof(OpenIddictScopeStoreResolver))] + [InlineData(typeof(OpenIddictTokenStoreResolver))] + public void UseEntityFrameworkCore_RegistersEntityFrameworkCoreStoreFactories(Type type) + { + // Arrange + var services = new ServiceCollection(); + var builder = services.AddOpenIddict().AddCore(); + + // Act + builder.UseEntityFrameworkCore(); + + // Assert + Assert.Contains(services, service => service.ImplementationType == type); + } + [Fact] - public void UseDefaultModels_KeyTypeDefaultsToString() + public void UseEntityFrameworkCore_KeyTypeDefaultsToString() { // Arrange var services = new ServiceCollection(); var builder = services.AddOpenIddict().AddCore(); // Act - builder.UseDefaultModels(); + builder.UseEntityFrameworkCore(); // Assert var provider = services.BuildServiceProvider(); @@ -36,14 +54,14 @@ namespace OpenIddict.Tests } [Fact] - public void UseDefaultModels_KeyTypeCanBeOverriden() + public void UseEntityFrameworkCore_KeyTypeCanBeOverriden() { // Arrange var services = new ServiceCollection(); var builder = services.AddOpenIddict().AddCore(); // Act - builder.UseDefaultModels(); + builder.UseEntityFrameworkCore().ReplaceDefaultEntities(); // Assert var provider = services.BuildServiceProvider(); diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs deleted file mode 100644 index 76da44ba..00000000 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace OpenIddict.EntityFrameworkCore.Tests -{ - public class OpenIddictExtensionsTests - { - [Theory] - [InlineData(typeof(OpenIddictApplicationStoreResolver))] - [InlineData(typeof(OpenIddictAuthorizationStoreResolver))] - [InlineData(typeof(OpenIddictScopeStoreResolver))] - [InlineData(typeof(OpenIddictTokenStoreResolver))] - public void AddEntityFrameworkCoreStores_RegistersEntityFrameworkCoreStoreFactories(Type type) - { - // Arrange - var services = new ServiceCollection(); - var builder = services.AddOpenIddict().AddCore(); - - // Act - builder.AddEntityFrameworkCoreStores(); - - // Assert - Assert.Contains(services, service => service.ImplementationType == type); - } - } -} diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs index 9d2d49c8..680cf32f 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs @@ -317,7 +317,14 @@ namespace OpenIddict.Server.Tests services.AddDistributedMemoryCache(); services.AddOpenIddict() - .AddCore(options => options.UseDefaultModels()) + .AddCore(options => + { + options.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + }) + .AddServer(options => configuration?.Invoke(options)); }); @@ -330,5 +337,10 @@ namespace OpenIddict.Server.Tests return new TestServer(builder); } + + public class OpenIddictApplication { } + public class OpenIddictAuthorization { } + public class OpenIddictScope { } + public class OpenIddictToken { } } } diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs index b57a9fb2..1599a740 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs @@ -16,7 +16,6 @@ using Moq; using Newtonsoft.Json; using Newtonsoft.Json.Bson; using OpenIddict.Abstractions; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs index 0f9d9190..6258422f 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Abstractions; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs index 3a7162c4..ec46e80b 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs @@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Abstractions; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs index adc3dbfa..215437fc 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs @@ -17,7 +17,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Moq; using OpenIddict.Abstractions; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs index ffd483a9..02868155 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs @@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Abstractions; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests @@ -1584,11 +1583,7 @@ namespace OpenIddict.Server.Tests public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted() { // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { @@ -1610,8 +1605,9 @@ namespace OpenIddict.Server.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); - options.AccessTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero)); + options.AccessTokenLifetime = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) - + new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero); }); }); @@ -1634,8 +1630,8 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => - descriptor.ExpirationDate == token.ExpirationDate && - descriptor.CreationDate == token.CreationDate && + descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && + descriptor.CreationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload != null && descriptor.ReferenceId != null && descriptor.Subject == "Bob le Magnifique" && @@ -1821,11 +1817,7 @@ namespace OpenIddict.Server.Tests public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted() { // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { @@ -1867,8 +1859,9 @@ namespace OpenIddict.Server.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); - options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + options.AuthorizationCodeLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - + new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); }); }); @@ -1887,8 +1880,8 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => - descriptor.ExpirationDate == token.ExpirationDate && - descriptor.CreationDate == token.CreationDate && + descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && + descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload == null && descriptor.ReferenceId == null && descriptor.Subject == "Bob le Magnifique" && @@ -1900,11 +1893,7 @@ namespace OpenIddict.Server.Tests public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted() { // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { @@ -1951,8 +1940,9 @@ namespace OpenIddict.Server.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); - options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + options.AuthorizationCodeLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - + new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); }); }); @@ -1974,8 +1964,8 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => - descriptor.ExpirationDate == token.ExpirationDate && - descriptor.CreationDate == token.CreationDate && + descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && + descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload != null && descriptor.ReferenceId != null && descriptor.Subject == "Bob le Magnifique" && @@ -2232,11 +2222,7 @@ namespace OpenIddict.Server.Tests public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted() { // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { @@ -2253,8 +2239,9 @@ namespace OpenIddict.Server.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); - options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - + new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); }); }); @@ -2274,8 +2261,8 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => - descriptor.ExpirationDate == token.ExpirationDate && - descriptor.CreationDate == token.CreationDate && + descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && + descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload == null && descriptor.ReferenceId == null && descriptor.Subject == "Bob le Magnifique" && @@ -2287,11 +2274,7 @@ namespace OpenIddict.Server.Tests public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted() { // Arrange - var token = new OpenIddictToken - { - CreationDate = new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero), - ExpirationDate = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - }; + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { @@ -2313,8 +2296,9 @@ namespace OpenIddict.Server.Tests builder.Configure(options => { - options.SystemClock = Mock.Of(mock => mock.UtcNow == token.CreationDate.Value); - options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; + options.SystemClock = Mock.Of(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero)); + options.RefreshTokenLifetime = new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) - + new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero); }); }); @@ -2337,8 +2321,8 @@ namespace OpenIddict.Server.Tests Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => - descriptor.ExpirationDate == token.ExpirationDate && - descriptor.CreationDate == token.CreationDate && + descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && + descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload != null && descriptor.ReferenceId == "B1F0D503-55A4-4B03-B05B-EF07713C18E1" && descriptor.Subject == "Bob le Magnifique" && diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs index 1614f90d..429081a1 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -30,7 +30,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Server.Tests @@ -1347,7 +1346,10 @@ namespace OpenIddict.Server.Tests services.AddOpenIddict() .AddCore(options => { - options.UseDefaultModels(); + options.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); options.Services.AddSingleton(CreateApplicationManager()); options.Services.AddSingleton(CreateAuthorizationManager()); @@ -1546,5 +1548,10 @@ namespace OpenIddict.Server.Tests return manager.Object; } + + public class OpenIddictApplication { } + public class OpenIddictAuthorization { } + public class OpenIddictScope { } + public class OpenIddictToken { } } } diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index 35dda1de..0216b863 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -671,7 +671,14 @@ namespace OpenIddict.Server.Tests private static OpenIddictServerBuilder CreateBuilder(IServiceCollection services) => services.AddOpenIddict() - .AddCore(options => options.UseDefaultModels()) + .AddCore(options => + { + options.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + }) + .AddServer(); private static IServiceCollection CreateServices() @@ -695,5 +702,10 @@ namespace OpenIddict.Server.Tests var options = provider.GetRequiredService>(); return options.Get(OpenIddictServerDefaults.AuthenticationScheme); } + + public class OpenIddictApplication { } + public class OpenIddictAuthorization { } + public class OpenIddictScope { } + public class OpenIddictToken { } } } diff --git a/test/OpenIddict.Tests/OpenIddict.Tests.csproj b/test/OpenIddict.Tests/OpenIddict.Tests.csproj deleted file mode 100644 index ef85a9bc..00000000 --- a/test/OpenIddict.Tests/OpenIddict.Tests.csproj +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - netcoreapp2.0;net461 - $(TargetFrameworks);net47 - netcoreapp2.0 - - - - - - - - - - - - - - - - - - - diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs index fdf7baca..b327802f 100755 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs @@ -27,7 +27,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; using OpenIddict.Core; -using OpenIddict.Models; using Xunit; namespace OpenIddict.Validation.Tests @@ -687,6 +686,11 @@ namespace OpenIddict.Validation.Tests services.AddOpenIddict() .AddCore(options => { + options.SetDefaultApplicationEntity() + .SetDefaultAuthorizationEntity() + .SetDefaultScopeEntity() + .SetDefaultTokenEntity(); + // Replace the default OpenIddict managers. options.Services.AddSingleton(CreateApplicationManager()); options.Services.AddSingleton(CreateAuthorizationManager()); @@ -824,5 +828,10 @@ namespace OpenIddict.Validation.Tests return manager.Object; } + + public class OpenIddictApplication { } + public class OpenIddictAuthorization { } + public class OpenIddictScope { } + public class OpenIddictToken { } } }