Browse Source

Remove OpenIddict.Models/OpenIddict.Stores and move to per-provider models

pull/609/head
Kévin Chalet 8 years ago
parent
commit
32fe4b0ae7
  1. 27
      OpenIddict.sln
  2. 2
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  3. 10
      samples/Mvc.Server/Startup.cs
  4. 2
      src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs
  5. 2
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  6. 2
      src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs
  7. 2
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  8. 5
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  9. 75
      src/OpenIddict.Abstractions/OpenIddictException.cs
  10. 2
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  11. 2
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  12. 2
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  13. 2
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  14. 234
      src/OpenIddict.Core/OpenIddictCoreBuilder.cs
  15. 3
      src/OpenIddict.Core/OpenIddictCoreOptions.cs
  16. 2
      src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj
  17. 105
      src/OpenIddict.EntityFramework.Models/OpenIddictApplication.cs
  18. 77
      src/OpenIddict.EntityFramework.Models/OpenIddictAuthorization.cs
  19. 2
      src/OpenIddict.EntityFramework.Models/OpenIddictScope.cs
  20. 97
      src/OpenIddict.EntityFramework.Models/OpenIddictToken.cs
  21. 2
      src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj
  22. 111
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkBuilder.cs
  23. 58
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs
  24. 25
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkOptions.cs
  25. 39
      src/OpenIddict.EntityFramework/Resolvers/OpenIddictApplicationStoreResolver.cs
  26. 39
      src/OpenIddict.EntityFramework/Resolvers/OpenIddictAuthorizationStoreResolver.cs
  27. 39
      src/OpenIddict.EntityFramework/Resolvers/OpenIddictScopeStoreResolver.cs
  28. 39
      src/OpenIddict.EntityFramework/Resolvers/OpenIddictTokenStoreResolver.cs
  29. 793
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  30. 623
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  31. 534
      src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs
  32. 698
      src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
  33. 15
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj
  34. 2
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs
  35. 5
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictAuthorization.cs
  36. 69
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictScope.cs
  37. 2
      src/OpenIddict.EntityFrameworkCore.Models/OpenIddictToken.cs
  38. 2
      src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj
  39. 123
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
  40. 6
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs
  41. 61
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs
  42. 25
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs
  43. 39
      src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictApplicationStoreResolver.cs
  44. 38
      src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictAuthorizationStoreResolver.cs
  45. 39
      src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictScopeStoreResolver.cs
  46. 39
      src/OpenIddict.EntityFrameworkCore/Resolvers/OpenIddictTokenStoreResolver.cs
  47. 649
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  48. 447
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  49. 420
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs
  50. 578
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
  51. 44
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs
  52. 10
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  53. 25
      src/OpenIddict.Stores/OpenIddict.Stores.csproj
  54. 832
      src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs
  55. 694
      src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs
  56. 577
      src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs
  57. 805
      src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs
  58. 5
      src/OpenIddict/OpenIddict.csproj
  59. 57
      src/OpenIddict/OpenIddictExtensions.cs
  60. 10
      test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs
  61. 57
      test/OpenIddict.EntityFramework.Tests/OpenIddictEntityFrameworkExtensionsTests.cs
  62. 35
      test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs
  63. 32
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs
  64. 34
      test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs
  65. 14
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs
  66. 1
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs
  67. 1
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs
  68. 1
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs
  69. 1
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs
  70. 76
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs
  71. 11
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs
  72. 14
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs
  73. 27
      test/OpenIddict.Tests/OpenIddict.Tests.csproj
  74. 11
      test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs

27
OpenIddict.sln

@ -38,25 +38,23 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFrameworkC
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Mvc.Tests", "test\OpenIddict.Mvc.Tests\OpenIddict.Mvc.Tests.csproj", "{8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Mvc.Tests", "test\OpenIddict.Mvc.Tests\OpenIddict.Mvc.Tests.csproj", "{8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Tests", "test\OpenIddict.Tests\OpenIddict.Tests.csproj", "{3E2FBDB3-DC82-4E97-8EBC-CC8B279110FF}" 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.Models", "src\OpenIddict.Models\OpenIddict.Models.csproj", "{0102A6CC-41A6-4B34-B49E-65AFE95882BB}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework", "src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj", "{BF42CC6C-0B56-4F66-9866-18B8393F3C06}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework", "src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj", "{BF42CC6C-0B56-4F66-9866-18B8393F3C06}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework.Tests", "test\OpenIddict.EntityFramework.Tests\OpenIddict.EntityFramework.Tests.csproj", "{96325E37-9897-43AC-8408-7B17F58E8788}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.EntityFramework.Tests", "test\OpenIddict.EntityFramework.Tests\OpenIddict.EntityFramework.Tests.csproj", "{96325E37-9897-43AC-8408-7B17F58E8788}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server", "src\OpenIddict.Server\OpenIddict.Server.csproj", "{21A7F241-CBE7-4F5C-9787-F2C50D135AEA}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", "test\OpenIddict.Server.Tests\OpenIddict.Server.Tests.csproj", "{07B02B98-8A68-432D-A932-48E6D52B221A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.Tests", "test\OpenIddict.Server.Tests\OpenIddict.Server.Tests.csproj", "{07B02B98-8A68-432D-A932-48E6D52B221A}"
EndProject 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Validation.Tests", "test\OpenIddict.Validation.Tests\OpenIddict.Validation.Tests.csproj", "{F470E734-F4B6-4355-AF32-53412B619E41}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.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.ActiveCfg = Release|Any CPU
{8B4B0CCC-711B-4F9D-9DE6-DD32BDD3BCCA}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{0102A6CC-41A6-4B34-B49E-65AFE95882BB}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{96325E37-9897-43AC-8408-7B17F58E8788}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{21A7F241-CBE7-4F5C-9787-F2C50D135AEA}.Debug|Any CPU.Build.0 = 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 {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}.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.ActiveCfg = Release|Any CPU
{6AB8F9E7-47F8-4A40-837F-C8753362AF54}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -154,16 +148,15 @@ Global
{A892B3DE-1A02-48D6-993B-DD3DFCAC84C7} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {A892B3DE-1A02-48D6-993B-DD3DFCAC84C7} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{7831F17A-DF0B-42EC-841B-065A9B5BD786} = {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} {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} {0102A6CC-41A6-4B34-B49E-65AFE95882BB} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{BF42CC6C-0B56-4F66-9866-18B8393F3C06} = {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} {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} {21A7F241-CBE7-4F5C-9787-F2C50D135AEA} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{07B02B98-8A68-432D-A932-48E6D52B221A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {07B02B98-8A68-432D-A932-48E6D52B221A} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{886A16DA-C9CF-4979-9B38-D06DF8A714B6} = {D544447C-D701-46BB-9A5B-C76C612A596B} {886A16DA-C9CF-4979-9B38-D06DF8A714B6} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7} {F470E734-F4B6-4355-AF32-53412B619E41} = {5FC71D6A-A994-4F62-977F-88A7D25379D7}
{6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B} {6AB8F9E7-47F8-4A40-837F-C8753362AF54} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{B5371534-4C33-41FA-B3D3-7D70D632DB15} = {D544447C-D701-46BB-9A5B-C76C612A596B}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616} SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

2
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -20,7 +20,7 @@ using Mvc.Server.ViewModels.Authorization;
using Mvc.Server.ViewModels.Shared; using Mvc.Server.ViewModels.Shared;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Server; using OpenIddict.Server;
namespace Mvc.Server namespace Mvc.Server

10
samples/Mvc.Server/Startup.cs

@ -10,7 +10,7 @@ using Mvc.Server.Models;
using Mvc.Server.Services; using Mvc.Server.Services;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace Mvc.Server namespace Mvc.Server
{ {
@ -71,11 +71,9 @@ namespace Mvc.Server
// Register the OpenIddict core services. // Register the OpenIddict core services.
.AddCore(options => .AddCore(options =>
{ {
// Configure OpenIddict to use the default models. // Configure OpenIddict to use the Entity Framework Core stores and models.
options.UseDefaultModels(); options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
// Register the Entity Framework stores.
options.AddEntityFrameworkCoreStores<ApplicationDbContext>();
}) })
// Register the OpenIddict server handler. // Register the OpenIddict server handler.

2
src/OpenIddict.Abstractions/Managers/IOpenIddictApplicationManager.cs

@ -287,7 +287,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.

2
src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs

@ -306,7 +306,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.

2
src/OpenIddict.Abstractions/Managers/IOpenIddictScopeManager.cs

@ -205,7 +205,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.

2
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -309,7 +309,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken = default); Task<ImmutableArray<object>> ListAsync([CanBeNull] int? count = null, [CanBeNull] int? offset = null, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.

5
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -39,6 +39,11 @@ namespace OpenIddict.Abstractions
public const string LogoutRequest = "openiddict-logout-request:"; public const string LogoutRequest = "openiddict-logout-request:";
} }
public static class Exceptions
{
public const string ConcurrencyError = "concurrency_error";
}
public static class Permissions public static class Permissions
{ {
public static class Endpoints public static class Endpoints

75
src/OpenIddict.Abstractions/OpenIddictException.cs

@ -0,0 +1,75 @@
using System;
using System.Runtime.Serialization;
namespace OpenIddict.Abstractions
{
/// <summary>
/// Represents an OpenIddict exception.
/// </summary>
public class OpenIddictException : Exception
{
/// <summary>
/// Creates a new <see cref="OpenIddictException"/>.
/// </summary>
/// <param name="reason">The reason of the exception.</param>
/// <param name="message">The exception message.</param>
public OpenIddictException(string reason, string message)
: base(message)
{
Reason = reason;
}
/// <summary>
/// Creates a new <see cref="OpenIddictException"/>.
/// </summary>
/// <param name="reason">The reason of the exception.</param>
/// <param name="message">The exception message.</param>
/// <param name="innerException">The inner exception.</param>
public OpenIddictException(string reason, string message, Exception innerException)
: base(message, innerException)
{
Reason = reason;
}
/// <summary>
/// Creates a new <see cref="OpenIddictException"/>.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains contextual information about the source or destination.
/// </param>
protected OpenIddictException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
Reason = info.GetString(nameof(Reason));
}
/// <summary>
/// Gets the reason that caused the exception to be thrown.
/// </summary>
public string Reason { get; }
/// <summary>
/// Serializes the members of this class.
/// </summary>
/// <param name="info">
/// The <see cref="SerializationInfo"/> that holds the serialized object data about the exception being thrown.
/// </param>
/// <param name="context">
/// The <see cref="StreamingContext"/> that contains contextual information about the source or destination.
/// </param>
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);
}
}
}

2
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. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TApplication>> ListAsync( public virtual Task<ImmutableArray<TApplication>> 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); return Store.ListAsync(count, offset, cancellationToken);
} }

2
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. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TAuthorization>> ListAsync( public virtual Task<ImmutableArray<TAuthorization>> 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); return Store.ListAsync(count, offset, cancellationToken);
} }

2
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. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TScope>> ListAsync( public virtual Task<ImmutableArray<TScope>> 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); return Store.ListAsync(count, offset, cancellationToken);
} }

2
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. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TToken>> ListAsync( public virtual Task<ImmutableArray<TToken>> 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); return Store.ListAsync(count, offset, cancellationToken);
} }

234
src/OpenIddict.Core/OpenIddictCoreBuilder.cs

@ -63,9 +63,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TStore">The type of the custom store.</typeparam> /// <typeparam name="TStore">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddApplicationStore<TStore>() where TStore : class public OpenIddictCoreBuilder AddApplicationStore<TStore>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> AddApplicationStore(typeof(TStore)); where TStore : class
=> AddApplicationStore(typeof(TStore), lifetime);
/// <summary> /// <summary>
/// Adds a custom application store by a custom implementation derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddApplicationStore([NotNull] Type type) public OpenIddictCoreBuilder AddApplicationStore(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -97,13 +101,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictApplicationStore<>) Services.Replace(new ServiceDescriptor(typeof(IOpenIddictApplicationStore<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -116,9 +120,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TStore">The type of the custom store.</typeparam> /// <typeparam name="TStore">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddAuthorizationStore<TStore>() where TStore : class public OpenIddictCoreBuilder AddAuthorizationStore<TStore>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> AddAuthorizationStore(typeof(TStore)); where TStore : class
=> AddAuthorizationStore(typeof(TStore), lifetime);
/// <summary> /// <summary>
/// Adds a custom authorization store by a custom implementation derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddAuthorizationStore([NotNull] Type type) public OpenIddictCoreBuilder AddAuthorizationStore(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -150,13 +158,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictAuthorizationStore<>) Services.Replace(new ServiceDescriptor(typeof(IOpenIddictAuthorizationStore<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -169,9 +177,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TStore">The type of the custom store.</typeparam> /// <typeparam name="TStore">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddScopeStore<TStore>() where TStore : class public OpenIddictCoreBuilder AddScopeStore<TStore>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> AddScopeStore(typeof(TStore)); where TStore : class
=> AddScopeStore(typeof(TStore), lifetime);
/// <summary> /// <summary>
/// Adds a custom scope store by a custom implementation derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddScopeStore([NotNull] Type type) public OpenIddictCoreBuilder AddScopeStore(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -203,13 +215,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictScopeStore<>) Services.Replace(new ServiceDescriptor(typeof(IOpenIddictScopeStore<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -222,9 +234,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TStore">The type of the custom store.</typeparam> /// <typeparam name="TStore">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddTokenStore<TStore>() where TStore : class public OpenIddictCoreBuilder AddTokenStore<TStore>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> AddTokenStore(typeof(TStore)); where TStore : class
=> AddTokenStore(typeof(TStore), lifetime);
/// <summary> /// <summary>
/// Adds a custom token store by a custom implementation derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder AddTokenStore([NotNull] Type type) public OpenIddictCoreBuilder AddTokenStore(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -256,18 +272,31 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(IOpenIddictTokenStore<>) Services.Replace(new ServiceDescriptor(typeof(IOpenIddictTokenStore<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
} }
/// <summary>
/// Replace the default application manager by a custom manager derived
/// from <see cref="OpenIddictApplicationManager{TApplication}"/>.
/// Note: when using this overload, the application manager
/// must be either a non-generic or closed generic service.
/// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TManager : class
=> ReplaceApplicationManager(typeof(TManager), lifetime);
/// <summary> /// <summary>
/// Replace the default application manager by a custom manager derived /// Replace the default application manager by a custom manager derived
/// from <see cref="OpenIddictApplicationManager{TApplication}"/>. /// from <see cref="OpenIddictApplicationManager{TApplication}"/>.
@ -275,8 +304,10 @@ namespace Microsoft.Extensions.DependencyInjection
/// either a non-generic, a closed or an open generic service. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationManager([NotNull] Type type) public OpenIddictCoreBuilder ReplaceApplicationManager(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -298,13 +329,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>) Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -314,17 +345,20 @@ namespace Microsoft.Extensions.DependencyInjection
/// Replaces the default application store resolver by a custom implementation. /// Replaces the default application store resolver by a custom implementation.
/// </summary> /// </summary>
/// <typeparam name="TResolver">The type of the custom store.</typeparam> /// <typeparam name="TResolver">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationStoreResolver<TResolver>() public OpenIddictCoreBuilder ReplaceApplicationStoreResolver<TResolver>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TResolver : IOpenIddictApplicationStoreResolver where TResolver : IOpenIddictApplicationStoreResolver
=> ReplaceApplicationStoreResolver(typeof(TResolver)); => ReplaceApplicationStoreResolver(typeof(TResolver), lifetime);
/// <summary> /// <summary>
/// Replaces the default application store resolver by a custom implementation. /// Replaces the default application store resolver by a custom implementation.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationStoreResolver([NotNull] Type type) public OpenIddictCoreBuilder ReplaceApplicationStoreResolver(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -336,22 +370,11 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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; return this;
} }
/// <summary>
/// Replace the default application manager by a custom manager derived
/// from <see cref="OpenIddictApplicationManager{TApplication}"/>.
/// Note: when using this overload, the application manager
/// must be either a non-generic or closed generic service.
/// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationManager<TManager>() where TManager : class
=> ReplaceApplicationManager(typeof(TManager));
/// <summary> /// <summary>
/// Replace the default authorization manager by a custom manager derived /// Replace the default authorization manager by a custom manager derived
/// from <see cref="OpenIddictAuthorizationManager{TAuthorization}"/>. /// from <see cref="OpenIddictAuthorizationManager{TAuthorization}"/>.
@ -359,9 +382,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationManager<TManager>() where TManager : class public OpenIddictCoreBuilder ReplaceAuthorizationManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> ReplaceAuthorizationManager(typeof(TManager)); where TManager : class
=> ReplaceAuthorizationManager(typeof(TManager), lifetime);
/// <summary> /// <summary>
/// Replace the default authorization manager by a custom manager derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationManager([NotNull] Type type) public OpenIddictCoreBuilder ReplaceAuthorizationManager(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -393,13 +420,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>) Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -409,17 +436,20 @@ namespace Microsoft.Extensions.DependencyInjection
/// Replaces the default authorization store resolver by a custom implementation. /// Replaces the default authorization store resolver by a custom implementation.
/// </summary> /// </summary>
/// <typeparam name="TResolver">The type of the custom store.</typeparam> /// <typeparam name="TResolver">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver<TResolver>() public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver<TResolver>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TResolver : IOpenIddictAuthorizationStoreResolver where TResolver : IOpenIddictAuthorizationStoreResolver
=> ReplaceAuthorizationStoreResolver(typeof(TResolver)); => ReplaceAuthorizationStoreResolver(typeof(TResolver), lifetime);
/// <summary> /// <summary>
/// Replaces the default authorization store resolver by a custom implementation. /// Replaces the default authorization store resolver by a custom implementation.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver([NotNull] Type type) public OpenIddictCoreBuilder ReplaceAuthorizationStoreResolver(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -431,7 +461,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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; return this;
} }
@ -444,8 +474,9 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeManager<TManager>() where TManager : class public OpenIddictCoreBuilder ReplaceScopeManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> ReplaceScopeManager(typeof(TManager)); where TManager : class
=> ReplaceScopeManager(typeof(TManager), lifetime);
/// <summary> /// <summary>
/// Replace the default scope manager by a custom manager /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeManager([NotNull] Type type) public OpenIddictCoreBuilder ReplaceScopeManager(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -477,13 +510,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>) Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -493,17 +526,20 @@ namespace Microsoft.Extensions.DependencyInjection
/// Replaces the default scope store resolver by a custom implementation. /// Replaces the default scope store resolver by a custom implementation.
/// </summary> /// </summary>
/// <typeparam name="TResolver">The type of the custom store.</typeparam> /// <typeparam name="TResolver">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeStoreResolver<TResolver>() public OpenIddictCoreBuilder ReplaceScopeStoreResolver<TResolver>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TResolver : IOpenIddictScopeStoreResolver where TResolver : IOpenIddictScopeStoreResolver
=> ReplaceScopeStoreResolver(typeof(TResolver)); => ReplaceScopeStoreResolver(typeof(TResolver), lifetime);
/// <summary> /// <summary>
/// Replaces the default scope store resolver by a custom implementation. /// Replaces the default scope store resolver by a custom implementation.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeStoreResolver([NotNull] Type type) public OpenIddictCoreBuilder ReplaceScopeStoreResolver(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -515,7 +551,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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; return this;
} }
@ -527,9 +563,11 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenManager<TManager>() where TManager : class public OpenIddictCoreBuilder ReplaceTokenManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
=> ReplaceTokenManager(typeof(TManager)); where TManager : class
=> ReplaceTokenManager(typeof(TManager), lifetime);
/// <summary> /// <summary>
/// Replace the default token manager by a custom manager /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenManager([NotNull] Type type) public OpenIddictCoreBuilder ReplaceTokenManager(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -561,13 +601,13 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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 else
{ {
Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>) Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type)); .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime));
} }
return this; return this;
@ -577,17 +617,20 @@ namespace Microsoft.Extensions.DependencyInjection
/// Replaces the default token store resolver by a custom implementation. /// Replaces the default token store resolver by a custom implementation.
/// </summary> /// </summary>
/// <typeparam name="TResolver">The type of the custom store.</typeparam> /// <typeparam name="TResolver">The type of the custom store.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenStoreResolver<TResolver>() public OpenIddictCoreBuilder ReplaceTokenStoreResolver<TResolver>(ServiceLifetime lifetime = ServiceLifetime.Scoped)
where TResolver : IOpenIddictTokenStoreResolver where TResolver : IOpenIddictTokenStoreResolver
=> ReplaceTokenStoreResolver(typeof(TResolver)); => ReplaceTokenStoreResolver(typeof(TResolver), lifetime);
/// <summary> /// <summary>
/// Replaces the default token store resolver by a custom implementation. /// Replaces the default token store resolver by a custom implementation.
/// </summary> /// </summary>
/// <param name="type">The type of the custom store.</param> /// <param name="type">The type of the custom store.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenStoreResolver([NotNull] Type type) public OpenIddictCoreBuilder ReplaceTokenStoreResolver(
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -599,30 +642,41 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); 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; return this;
} }
/// <summary> /// <summary>
/// Configures OpenIddict to use the specified entities. /// Configures OpenIddict to use the specified entity as the default application entity.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder SetDefaultApplicationEntity<TApplication>()
where TApplication : class, new()
=> Configure(options => options.DefaultApplicationType = typeof(TApplication));
/// <summary>
/// Configures OpenIddict to use the specified entity as the default authorization entity.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder SetDefaultAuthorizationEntity<TAuthorization>()
where TAuthorization : class, new()
=> Configure(options => options.DefaultAuthorizationType = typeof(TAuthorization));
/// <summary>
/// Configures OpenIddict to use the specified entity as the default scope entity.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder SetDefaultScopeEntity<TScope>()
where TScope : class, new()
=> Configure(options => options.DefaultScopeType = typeof(TScope));
/// <summary>
/// Configures OpenIddict to use the specified entity as the default token entity.
/// </summary> /// </summary>
/// <typeparam name="TApplication">The type corresponding to the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type corresponding to the Authorization entity.</typeparam>
/// <typeparam name="TScope">The type corresponding to the Scope entity.</typeparam>
/// <typeparam name="TToken">The type corresponding to the Token entity.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder UseCustomModels<TApplication, TAuthorization, TScope, TToken>() public OpenIddictCoreBuilder SetDefaultTokenEntity<TToken>()
where TApplication : class where TToken : class, new()
where TAuthorization : class => Configure(options => options.DefaultTokenType = typeof(TToken));
where TScope : class
where TToken : class
=> Configure(options =>
{
options.DefaultApplicationType = typeof(TApplication);
options.DefaultAuthorizationType = typeof(TAuthorization);
options.DefaultScopeType = typeof(TScope);
options.DefaultTokenType = typeof(TToken);
});
} }
} }

3
src/OpenIddict.Core/OpenIddictCoreOptions.cs

@ -8,6 +8,9 @@ using System;
namespace OpenIddict.Core namespace OpenIddict.Core
{ {
/// <summary>
/// Provides various settings needed to configure the OpenIddict core services.
/// </summary>
public class OpenIddictCoreOptions public class OpenIddictCoreOptions
{ {
/// <summary> /// <summary>

2
src/OpenIddict.Models/OpenIddict.Models.csproj → src/OpenIddict.EntityFramework.Models/OpenIddict.EntityFramework.Models.csproj

@ -7,7 +7,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Description>OpenIddict's default entities, used by the Entity Framework 6.x and Entity Framework Core stores.</Description> <Description>Relational entities for the Entity Framework 6.x stores.</Description>
<Authors>Kévin Chalet</Authors> <Authors>Kévin Chalet</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags> <PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup> </PropertyGroup>

105
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
{
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictApplication : OpenIddictApplication<string, OpenIddictAuthorization, OpenIddictToken>
{
public OpenIddictApplication()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
}
/// <summary>
/// Represents an OpenIddict application.
/// </summary>
public class OpenIddictApplication<TKey, TAuthorization, TToken> where TKey : IEquatable<TKey>
{
/// <summary>
/// Gets the list of the authorizations associated with this application.
/// </summary>
public virtual IList<TAuthorization> Authorizations { get; } = new List<TAuthorization>();
/// <summary>
/// Gets or sets the client identifier
/// associated with the current application.
/// </summary>
public virtual string ClientId { get; set; }
/// <summary>
/// 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.
/// </summary>
public virtual string ClientSecret { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the consent type
/// associated with the current application.
/// </summary>
public virtual string ConsentType { get; set; }
/// <summary>
/// Gets or sets the display name
/// associated with the current application.
/// </summary>
public virtual string DisplayName { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current application.
/// </summary>
public virtual TKey Id { get; set; }
/// <summary>
/// Gets or sets the permissions associated with the
/// current application, serialized as a JSON array.
/// </summary>
public virtual string Permissions { get; set; }
/// <summary>
/// Gets or sets the logout callback URLs associated with
/// the current application, serialized as a JSON array.
/// </summary>
public virtual string PostLogoutRedirectUris { get; set; }
/// <summary>
/// Gets or sets the additional properties serialized as a JSON object,
/// or <c>null</c> if no bag was associated with the current application.
/// </summary>
public virtual string Properties { get; set; }
/// <summary>
/// Gets or sets the callback URLs associated with the
/// current application, serialized as a JSON array.
/// </summary>
public virtual string RedirectUris { get; set; }
/// <summary>
/// Gets the list of the tokens associated with this application.
/// </summary>
public virtual IList<TToken> Tokens { get; } = new List<TToken>();
/// <summary>
/// Gets or sets the application type
/// associated with the current application.
/// </summary>
public virtual string Type { get; set; }
}
}

77
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
{
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictAuthorization : OpenIddictAuthorization<string, OpenIddictApplication, OpenIddictToken>
{
public OpenIddictAuthorization()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
}
/// <summary>
/// Represents an OpenIddict authorization.
/// </summary>
public class OpenIddictAuthorization<TKey, TApplication, TToken> where TKey : IEquatable<TKey>
{
/// <summary>
/// Gets or sets the application associated with the current authorization.
/// </summary>
public virtual TApplication Application { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current authorization.
/// </summary>
public virtual TKey Id { get; set; }
/// <summary>
/// Gets or sets the additional properties serialized as a JSON object,
/// or <c>null</c> if no bag was associated with the current authorization.
/// </summary>
public virtual string Properties { get; set; }
/// <summary>
/// Gets or sets the scopes associated with the current
/// authorization, serialized as a JSON array.
/// </summary>
public virtual string Scopes { get; set; }
/// <summary>
/// Gets or sets the status of the current authorization.
/// </summary>
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current authorization.
/// </summary>
public virtual string Subject { get; set; }
/// <summary>
/// Gets the list of tokens associated with the current authorization.
/// </summary>
public virtual IList<TToken> Tokens { get; } = new List<TToken>();
/// <summary>
/// Gets or sets the type of the current authorization.
/// </summary>
public virtual string Type { get; set; }
}
}

2
src/OpenIddict.Models/OpenIddictScope.cs → src/OpenIddict.EntityFramework.Models/OpenIddictScope.cs

@ -6,7 +6,7 @@
using System; using System;
namespace OpenIddict.Models namespace OpenIddict.EntityFramework.Models
{ {
/// <summary> /// <summary>
/// Represents an OpenIddict scope. /// Represents an OpenIddict scope.

97
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
{
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictToken : OpenIddictToken<string, OpenIddictApplication, OpenIddictAuthorization>
{
public OpenIddictToken()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
}
/// <summary>
/// Represents an OpenIddict token.
/// </summary>
public class OpenIddictToken<TKey, TApplication, TAuthorization> where TKey : IEquatable<TKey>
{
/// <summary>
/// Gets or sets the application associated with the current token.
/// </summary>
public virtual TApplication Application { get; set; }
/// <summary>
/// Gets or sets the authorization associated with the current token.
/// </summary>
public virtual TAuthorization Authorization { get; set; }
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the date on which the token
/// will start to be considered valid.
/// </summary>
public virtual DateTimeOffset? CreationDate { get; set; }
/// <summary>
/// Gets or sets the date on which the token
/// will no longer be considered valid.
/// </summary>
public virtual DateTimeOffset? ExpirationDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current token.
/// </summary>
public virtual TKey Id { get; set; }
/// <summary>
/// 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.
/// </summary>
public virtual string Payload { get; set; }
/// <summary>
/// Gets or sets the additional properties serialized as a JSON object,
/// or <c>null</c> if no bag was associated with the current token.
/// </summary>
public virtual string Properties { get; set; }
/// <summary>
/// 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.
/// </summary>
public virtual string ReferenceId { get; set; }
/// <summary>
/// Gets or sets the status of the current token.
/// </summary>
public virtual string Status { get; set; }
/// <summary>
/// Gets or sets the subject associated with the current token.
/// </summary>
public virtual string Subject { get; set; }
/// <summary>
/// Gets or sets the type of the current token.
/// </summary>
public virtual string Type { get; set; }
}
}

2
src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj

@ -14,7 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" /> <ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.Stores\OpenIddict.Stores.csproj" /> <ProjectReference Include="..\OpenIddict.EntityFramework.Models\OpenIddict.EntityFramework.Models.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

111
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
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict Entity Framework 6.x services.
/// </summary>
public class OpenIddictEntityFrameworkBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictEntityFrameworkBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictEntityFrameworkBuilder([NotNull] IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
Services = services;
}
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict Entity Framework 6.x configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictEntityFrameworkBuilder"/>.</returns>
public OpenIddictEntityFrameworkBuilder Configure([NotNull] Action<OpenIddictEntityFrameworkOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Configures the OpenIddict Entity Framework 6.x stores to use the specified database context type.
/// </summary>
/// <typeparam name="TContext">The type of the <see cref="DbContext"/> used by OpenIddict.</typeparam>
/// <returns>The <see cref="OpenIddictEntityFrameworkBuilder"/>.</returns>
public OpenIddictEntityFrameworkBuilder UseDbContext<TContext>()
where TContext : DbContext => UseDbContext(typeof(TContext));
/// <summary>
/// Configures the OpenIddict Entity Framework 6.x stores to use the specified database context type.
/// </summary>
/// <param name="type">The type of the <see cref="DbContext"/> used by OpenIddict.</param>
/// <returns>The <see cref="OpenIddictEntityFrameworkBuilder"/>.</returns>
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);
}
/// <summary>
/// Configures OpenIddict to use the specified entities, derived
/// from the default OpenIddict Entity Framework 6.x entities.
/// </summary>
/// <returns>The <see cref="OpenIddictEntityFrameworkBuilder"/>.</returns>
public OpenIddictEntityFrameworkBuilder ReplaceDefaultEntities<TApplication, TAuthorization, TScope, TToken, TKey>()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TScope : OpenIddictScope<TKey>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
Services.Configure<OpenIddictCoreOptions>(options =>
{
options.DefaultApplicationType = typeof(TApplication);
options.DefaultAuthorizationType = typeof(TAuthorization);
options.DefaultScopeType = typeof(TScope);
options.DefaultTokenType = typeof(TToken);
});
return this;
}
}
}

58
src/OpenIddict.EntityFramework/OpenIddictExtensions.cs → src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs

@ -12,37 +12,69 @@ using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenIddict.EntityFramework; using OpenIddict.EntityFramework;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
public static class OpenIddictExtensions public static class OpenIddictEntityFrameworkExtensions
{ {
/// <summary> /// <summary>
/// Registers the Entity Framework 6.x stores. Note: when using the Entity Framework stores, /// Registers the Entity Framework 6.x stores services in the DI container and
/// the application <see cref="DbContext"/> MUST be manually registered in the DI container and /// configures OpenIddict to use the Entity Framework 6.x entities by default.
/// the entities MUST be derived from the models contained in the OpenIddict.Models package.
/// </summary> /// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param> /// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks> /// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictEntityFrameworkBuilder"/>.</returns>
public static OpenIddictCoreBuilder AddEntityFrameworkStores<TContext>([NotNull] this OpenIddictCoreBuilder builder) public static OpenIddictEntityFrameworkBuilder UseEntityFramework([NotNull] this OpenIddictCoreBuilder builder)
where TContext : DbContext
{ {
if (builder == null) if (builder == null)
{ {
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
builder.ReplaceApplicationStoreResolver<OpenIddictApplicationStoreResolver>()
.ReplaceAuthorizationStoreResolver<OpenIddictAuthorizationStoreResolver>()
.ReplaceScopeStoreResolver<OpenIddictScopeStoreResolver>()
.ReplaceTokenStoreResolver<OpenIddictTokenStoreResolver>();
builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>));
builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>));
builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>));
builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>));
return builder.ReplaceApplicationStoreResolver<OpenIddictApplicationStoreResolver<TContext>>() return new OpenIddictEntityFrameworkBuilder(builder.Services);
.ReplaceAuthorizationStoreResolver<OpenIddictAuthorizationStoreResolver<TContext>>() }
.ReplaceScopeStoreResolver<OpenIddictScopeStoreResolver<TContext>>()
.ReplaceTokenStoreResolver<OpenIddictTokenStoreResolver<TContext>>(); /// <summary>
/// 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.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the Entity Framework 6.x services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder UseEntityFramework(
[NotNull] this OpenIddictCoreBuilder builder,
[NotNull] Action<OpenIddictEntityFrameworkBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseEntityFramework());
return builder;
} }
/// <summary> /// <summary>
@ -77,7 +109,7 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(builder)); 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. // always throws an exception when using generic types as entity types.
// To ensure a better exception is thrown, a manual check is made here. // To ensure a better exception is thrown, a manual check is made here.
if (typeof(TApplication).IsGenericType) if (typeof(TApplication).IsGenericType)

25
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
{
/// <summary>
/// Provides various settings needed to configure
/// the OpenIddict Entity Framework 6.x integration.
/// </summary>
public class OpenIddictEntityFrameworkOptions
{
/// <summary>
/// Gets or sets the concrete type of the <see cref="DbContext"/> 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.
/// </summary>
public Type ContextType { get; set; }
}
}

39
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.Collections.Concurrent;
using System.Data.Entity;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve an application store. /// Exposes a method allowing to resolve an application store.
/// </summary> /// </summary>
public class OpenIddictApplicationStoreResolver<TContext> : IOpenIddictApplicationStoreResolver public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictApplicationStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified application type is not compatible with the Entity Framework 6.x stores.") .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("When enabling the Entity Framework 6.x stores, make sure you use the built-in ")
.Append("'OpenIddictApplication' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictApplication' entity (from the 'OpenIddict.EntityFramework.Models' package) ")
.Append("that inherits from the generic 'OpenIddictApplication' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework
/* TApplication: */ key, /* TApplication: */ key,
/* TAuthorization: */ root.GenericTypeArguments[1], /* TAuthorization: */ root.GenericTypeArguments[1],
/* TToken: */ root.GenericTypeArguments[2], /* TToken: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

39
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.Collections.Concurrent;
using System.Data.Entity;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve an authorization store. /// Exposes a method allowing to resolve an authorization store.
/// </summary> /// </summary>
public class OpenIddictAuthorizationStoreResolver<TContext> : IOpenIddictAuthorizationStoreResolver public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictAuthorizationStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified authorization type is not compatible with the Entity Framework 6.x stores.") .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("When enabling the Entity Framework 6.x stores, make sure you use the built-in ")
.Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.EntityFramework.Models' package) ")
.Append("that inherits from the generic 'OpenIddictAuthorization' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework
/* TAuthorization: */ key, /* TAuthorization: */ key,
/* TApplication: */ root.GenericTypeArguments[1], /* TApplication: */ root.GenericTypeArguments[1],
/* TToken: */ root.GenericTypeArguments[2], /* TToken: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

39
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.Collections.Concurrent;
using System.Data.Entity;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve a scope store. /// Exposes a method allowing to resolve a scope store.
/// </summary> /// </summary>
public class OpenIddictScopeStoreResolver<TContext> : IOpenIddictScopeStoreResolver public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) public OpenIddictScopeStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,15 +54,25 @@ namespace OpenIddict.EntityFramework
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified scope type is not compatible with the Entity Framework 6.x stores.") .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("When enabling the Entity Framework 6.x stores, make sure you use the built-in ")
.Append("'OpenIdScope' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIdScope' entity (from the 'OpenIddict.EntityFramework.Models' package) ")
.Append("that inherits from the generic 'OpenIddictScope' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
return typeof(OpenIddictScopeStore<,,>).MakeGenericType( return typeof(OpenIddictScopeStore<,,>).MakeGenericType(
/* TScope: */ key, /* TScope: */ key,
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

39
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.Collections.Concurrent;
using System.Data.Entity;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve a token store. /// Exposes a method allowing to resolve a token store.
/// </summary> /// </summary>
public class OpenIddictTokenStoreResolver<TContext> : IOpenIddictTokenStoreResolver public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) public OpenIddictTokenStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +54,19 @@ namespace OpenIddict.EntityFramework
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified token type is not compatible with the Entity Framework 6.x stores.") .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("When enabling the Entity Framework 6.x stores, make sure you use the built-in ")
.Append("'OpenIddictToken' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictToken' entity (from the 'OpenIddict.EntityFramework.Models' package) ")
.Append("that inherits from the generic 'OpenIddictToken' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +74,7 @@ namespace OpenIddict.EntityFramework
/* TToken: */ key, /* TToken: */ key,
/* TApplication: */ root.GenericTypeArguments[1], /* TApplication: */ root.GenericTypeArguments[1],
/* TAuthorization: */ root.GenericTypeArguments[2], /* TAuthorization: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

793
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs

@ -7,21 +7,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.Entity; using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Models; using Newtonsoft.Json;
using OpenIddict.Stores; using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in a database. /// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictApplicationStore<TContext> : OpenIddictApplicationStore<OpenIddictApplication, public class OpenIddictApplicationStore<TContext> : OpenIddictApplicationStore<OpenIddictApplication,
@ -29,77 +33,68 @@ namespace OpenIddict.EntityFramework
OpenIddictToken, TContext, string> OpenIddictToken, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in a database. /// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictApplicationStore<TContext, TKey> : OpenIddictApplicationStore<OpenIddictApplication<TKey>,
OpenIddictAuthorization<TKey>,
OpenIddictToken<TKey>, TContext, TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
}
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TContext, TKey> : public class OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TContext, TKey> : IOpenIddictApplicationStore<TApplication>
OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TKey>
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of applications that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Applications.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of applications that match the specified query. /// Determines the number of applications that match the specified query.
@ -111,7 +106,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query. /// whose result returns the number of applications that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -129,7 +124,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -149,7 +144,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -205,9 +200,158 @@ namespace OpenIddict.EntityFramework
Applications.Remove(application); Applications.Remove(application);
await Context.SaveChangesAsync(cancellationToken); try
transaction?.Commit(); {
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);
}
}
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> 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();
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> 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();
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication>();
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();
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication>();
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();
} }
/// <summary> /// <summary>
@ -222,7 +366,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -234,6 +378,296 @@ namespace OpenIddict.EntityFramework
return query(Applications, state).FirstOrDefaultAsync(cancellationToken); return query(Applications, state).FirstOrDefaultAsync(cancellationToken);
} }
/// <summary>
/// Retrieves the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientId);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client secret associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientSecret);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
/// </returns>
public virtual ValueTask<string> GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.Type);
}
/// <summary>
/// Retrieves the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
/// </returns>
public virtual ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ConsentType);
}
/// <summary>
/// Retrieves the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(ConvertIdentifierToString(application.Id));
}
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Permissions))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(permissions);
}
/// <summary>
/// Retrieves the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.PostLogoutRedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the application.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(application.Properties));
}
/// <summary>
/// Retrieves the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.RedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated application, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TApplication> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TApplication>(new TApplication());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -246,7 +680,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -258,6 +692,232 @@ namespace OpenIddict.EntityFramework
return ImmutableArray.CreateRange(await query(Applications, state).ToListAsync(cancellationToken)); return ImmutableArray.CreateRange(await query(Applications, state).ToListAsync(cancellationToken));
} }
/// <summary>
/// Sets the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The client secret associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The client type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The consent type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="name">The display name associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="permissions">The permissions associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="properties">The additional properties associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary> /// <summary>
/// Updates an existing application. /// Updates an existing application.
/// </summary> /// </summary>
@ -266,7 +926,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -281,7 +941,48 @@ namespace OpenIddict.EntityFramework
Context.Entry(application).State = EntityState.Modified; 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

623
src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs

@ -7,22 +7,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.Entity; using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
using OpenIddict.Stores;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in a database. /// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictAuthorizationStore<TContext> : OpenIddictAuthorizationStore<OpenIddictAuthorization, public class OpenIddictAuthorizationStore<TContext> : OpenIddictAuthorizationStore<OpenIddictAuthorization,
@ -30,77 +33,68 @@ namespace OpenIddict.EntityFramework
OpenIddictToken, TContext, string> OpenIddictToken, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in a database. /// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictAuthorizationStore<TContext, TKey> : OpenIddictAuthorizationStore<OpenIddictAuthorization<TKey>,
OpenIddictApplication<TKey>,
OpenIddictToken<TKey>, TContext, TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
}
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TContext, TKey> : public class OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TContext, TKey> : IOpenIddictAuthorizationStore<TAuthorization>
OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TKey>
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of authorizations that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Authorizations.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of authorizations that match the specified query. /// Determines the number of authorizations that match the specified query.
@ -112,7 +106,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query. /// whose result returns the number of authorizations that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -130,7 +124,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -150,7 +144,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -188,9 +182,189 @@ namespace OpenIddict.EntityFramework
Authorizations.Remove(authorization); Authorizations.Remove(authorization);
await Context.SaveChangesAsync(cancellationToken); try
transaction?.Commit(); {
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);
}
}
}
/// <summary>
/// Retrieves the authorizations corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public virtual Task<TAuthorization> 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);
}
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
} }
/// <summary> /// <summary>
@ -202,7 +376,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the authorization. /// whose result returns the application identifier associated with the authorization.
/// </returns> /// </returns>
public override async ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -241,7 +415,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -250,7 +424,173 @@ namespace OpenIddict.EntityFramework
throw new ArgumentNullException(nameof(query)); 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);
}
/// <summary>
/// Retrieves the unique identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(ConvertIdentifierToString(authorization.Id));
}
/// <summary>
/// Retrieves the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the authorization.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(authorization.Properties));
}
/// <summary>
/// Retrieves the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Scopes))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray());
}
/// <summary>
/// Retrieves the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Status);
}
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Subject);
}
/// <summary>
/// Retrieves the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Type);
}
/// <summary>
/// Instantiates a new authorization.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated authorization, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TAuthorization> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TAuthorization>(new TAuthorization());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
} }
/// <summary> /// <summary>
@ -265,7 +605,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -285,7 +625,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
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 // 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. // entities in a single command without having to retrieve and materialize them first.
@ -293,14 +633,6 @@ namespace OpenIddict.EntityFramework
IList<Exception> exceptions = null; IList<Exception> exceptions = null;
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> 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() DbContextTransaction CreateTransaction()
{ {
// Note: relational providers like Sqlite are known to lack proper support // 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. // and thus prevent them from being concurrently modified outside this block.
using (var transaction = CreateTransaction()) using (var transaction = CreateTransaction())
{ {
var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); var authorizations =
if (authorizations.IsEmpty) 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; break;
} }
@ -373,7 +712,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, public virtual async Task SetApplicationIdAsync([NotNull] TAuthorization authorization,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
@ -410,6 +749,129 @@ namespace OpenIddict.EntityFramework
} }
} }
/// <summary>
/// Sets the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="properties">The additional properties associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="scopes">The scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetScopesAsync([NotNull] TAuthorization authorization,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="type">The type associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary> /// <summary>
/// Updates an existing authorization. /// Updates an existing authorization.
/// </summary> /// </summary>
@ -418,7 +880,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -433,7 +895,48 @@ namespace OpenIddict.EntityFramework
Context.Entry(authorization).State = EntityState.Modified; 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

534
src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs

@ -6,81 +6,77 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data.Entity; using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Core; using Newtonsoft.Json;
using OpenIddict.Models; using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenIddict.EntityFramework.Models;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in a database. /// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictScopeStore<TContext> : OpenIddictScopeStore<OpenIddictScope, TContext, string> public class OpenIddictScopeStore<TContext> : OpenIddictScopeStore<OpenIddictScope, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictScopeStore( public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
[NotNull] TContext context, : base(cache, context)
[NotNull] IMemoryCache cache)
: base(context, cache)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in a database. /// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictScopeStore<TContext, TKey> : OpenIddictScopeStore<OpenIddictScope<TKey>, TContext, TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
}
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam> /// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictScopeStore<TScope, TContext, TKey> : Stores.OpenIddictScopeStore<TScope, TKey> public class OpenIddictScopeStore<TScope, TContext, TKey> : IOpenIddictScopeStore<TScope>
where TScope : OpenIddictScope<TKey>, new() where TScope : OpenIddictScope<TKey>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TScope"/> entity. /// Gets the database set corresponding to the <typeparamref name="TScope"/> entity.
/// </summary> /// </summary>
protected DbSet<TScope> Scopes => Context.Set<TScope>(); private DbSet<TScope> Scopes => Context.Set<TScope>();
/// <summary>
/// Determines the number of scopes that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Scopes.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of scopes that match the specified query. /// Determines the number of scopes that match the specified query.
@ -92,7 +88,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query. /// whose result returns the number of scopes that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -110,7 +106,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -130,7 +126,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -139,7 +135,125 @@ namespace OpenIddict.EntityFramework
Scopes.Remove(scope); 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);
}
}
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public virtual Task<TScope> 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);
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual Task<TScope> 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);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> 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));
}
/// <summary>
/// Retrieves all the scopes that contain the specified resource.
/// </summary>
/// <param name="resource">The resource associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> 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<TScope>();
foreach (var scope in scopes)
{
var resources = await GetResourcesAsync(scope, cancellationToken);
if (resources.Contains(resource, StringComparer.OrdinalIgnoreCase))
{
builder.Add(scope);
}
}
return builder.ToImmutable();
} }
/// <summary> /// <summary>
@ -154,7 +268,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -166,6 +280,182 @@ namespace OpenIddict.EntityFramework
return query(Scopes, state).FirstOrDefaultAsync(cancellationToken); return query(Scopes, state).FirstOrDefaultAsync(cancellationToken);
} }
/// <summary>
/// Retrieves the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the description associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Description);
}
/// <summary>
/// Retrieves the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the scope.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the scope.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(ConvertIdentifierToString(scope.Id));
}
/// <summary>
/// Retrieves the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the name associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Name);
}
/// <summary>
/// Retrieves the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the scope.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(scope.Properties));
}
/// <summary>
/// Retrieves the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the resources associated with the scope.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Resources))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(resources);
}
/// <summary>
/// Instantiates a new scope.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated scope, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TScope> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TScope>(new TScope());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -178,7 +468,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -190,6 +480,125 @@ namespace OpenIddict.EntityFramework
return ImmutableArray.CreateRange(await query(Scopes, state).ToListAsync(cancellationToken)); return ImmutableArray.CreateRange(await query(Scopes, state).ToListAsync(cancellationToken));
} }
/// <summary>
/// Sets the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="description">The description associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The display name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The name associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="properties">The additional properties associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="resources">The resources associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray<string> 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;
}
/// <summary> /// <summary>
/// Updates an existing scope. /// Updates an existing scope.
/// </summary> /// </summary>
@ -198,7 +607,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -213,7 +622,48 @@ namespace OpenIddict.EntityFramework
Context.Entry(scope).State = EntityState.Modified; 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

698
src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs

@ -7,22 +7,25 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.Entity; using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models; using OpenIddict.EntityFramework.Models;
using OpenIddict.Stores;
namespace OpenIddict.EntityFramework namespace OpenIddict.EntityFramework
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in a database. /// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictTokenStore<TContext> : OpenIddictTokenStore<OpenIddictToken, public class OpenIddictTokenStore<TContext> : OpenIddictTokenStore<OpenIddictToken,
@ -30,77 +33,68 @@ namespace OpenIddict.EntityFramework
OpenIddictAuthorization, TContext, string> OpenIddictAuthorization, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in a database. /// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictTokenStore<TContext, TKey> : OpenIddictTokenStore<OpenIddictToken<TKey>,
OpenIddictApplication<TKey>,
OpenIddictAuthorization<TKey>, TContext, TKey>
where TContext : DbContext
where TKey : IEquatable<TKey>
{
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache)
: base(context, cache)
{
}
}
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictTokenStore<TToken, TApplication, TAuthorization, TContext, TKey> : public class OpenIddictTokenStore<TToken, TApplication, TAuthorization, TContext, TKey> : IOpenIddictTokenStore<TToken>
OpenIddictTokenStore<TToken, TApplication, TAuthorization, TKey>
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of tokens that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Tokens.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of tokens that match the specified query. /// Determines the number of tokens that match the specified query.
@ -112,7 +106,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query. /// whose result returns the number of tokens that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -130,7 +124,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -147,8 +141,10 @@ namespace OpenIddict.EntityFramework
/// </summary> /// </summary>
/// <param name="token">The token to delete.</param> /// <param name="token">The token to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns> /// <returns>
public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -157,7 +153,135 @@ namespace OpenIddict.EntityFramework
Tokens.Remove(token); 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);
}
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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));
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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));
}
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public virtual Task<TToken> 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);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public virtual Task<TToken> 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);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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));
} }
/// <summary> /// <summary>
@ -169,7 +293,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the token. /// whose result returns the application identifier associated with the token.
/// </returns> /// </returns>
public override async ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -208,7 +332,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -231,7 +355,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the token. /// whose result returns the authorization identifier associated with the token.
/// </returns> /// </returns>
public override async ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -258,6 +382,226 @@ namespace OpenIddict.EntityFramework
return ConvertIdentifierToString(token.Authorization.Id); return ConvertIdentifierToString(token.Authorization.Id);
} }
/// <summary>
/// Retrieves the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.CreationDate);
}
/// <summary>
/// Retrieves the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the expiration date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.ExpirationDate);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(ConvertIdentifierToString(token.Id));
}
/// <summary>
/// Retrieves the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the payload associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Payload);
}
/// <summary>
/// Retrieves the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the token.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(token.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(token.Properties));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the reference identifier associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.ReferenceId);
}
/// <summary>
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Status);
}
/// <summary>
/// Retrieves the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Subject);
}
/// <summary>
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Type);
}
/// <summary>
/// Instantiates a new token.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated token, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TToken> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TToken>(new TToken());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -270,7 +614,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -291,7 +635,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
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 // 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. // entities in a single command without having to retrieve and materialize them first.
@ -299,13 +643,6 @@ namespace OpenIddict.EntityFramework
IList<Exception> exceptions = null; IList<Exception> exceptions = null;
IQueryable<TToken> Query(IQueryable<TToken> 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() DbContextTransaction CreateTransaction()
{ {
// Note: relational providers like Sqlite are known to lack proper support // 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. // and thus prevent them from being concurrently modified outside this block.
using (var transaction = CreateTransaction()) using (var transaction = CreateTransaction())
{ {
var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); var tokens =
if (tokens.IsEmpty) 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; break;
} }
@ -373,7 +716,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetApplicationIdAsync([NotNull] TToken token, public virtual async Task SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
@ -419,7 +762,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetAuthorizationIdAsync([NotNull] TToken token, public virtual async Task SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
@ -456,6 +799,200 @@ namespace OpenIddict.EntityFramework
} }
} }
/// <summary>
/// Sets the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The creation date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The expiration date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="payload">The payload associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="properties">The additional properties associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The reference identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The token type associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary> /// <summary>
/// Updates an existing token. /// Updates an existing token.
/// </summary> /// </summary>
@ -464,7 +1001,7 @@ namespace OpenIddict.EntityFramework
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -479,7 +1016,48 @@ namespace OpenIddict.EntityFramework
Context.Entry(token).State = EntityState.Modified; 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

15
src/OpenIddict.EntityFrameworkCore.Models/OpenIddict.EntityFrameworkCore.Models.csproj

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>Relational entities for the Entity Framework Core stores.</Description>
<Authors>Kévin Chalet</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup>
</Project>

2
src/OpenIddict.Models/OpenIddictApplication.cs → src/OpenIddict.EntityFrameworkCore.Models/OpenIddictApplication.cs

@ -7,7 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenIddict.Models namespace OpenIddict.EntityFrameworkCore.Models
{ {
/// <summary> /// <summary>
/// Represents an OpenIddict application. /// Represents an OpenIddict application.

5
src/OpenIddict.Models/OpenIddictAuthorization.cs → src/OpenIddict.EntityFrameworkCore.Models/OpenIddictAuthorization.cs

@ -7,7 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace OpenIddict.Models namespace OpenIddict.EntityFrameworkCore.Models
{ {
/// <summary> /// <summary>
/// Represents an OpenIddict authorization. /// Represents an OpenIddict authorization.
@ -72,8 +72,7 @@ namespace OpenIddict.Models
public virtual string Subject { get; set; } public virtual string Subject { get; set; }
/// <summary> /// <summary>
/// Gets or sets the list of tokens /// Gets the list of tokens associated with the current authorization.
/// associated with the current authorization.
/// </summary> /// </summary>
public virtual IList<TToken> Tokens { get; } = new List<TToken>(); public virtual IList<TToken> Tokens { get; } = new List<TToken>();

69
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
{
/// <summary>
/// Represents an OpenIddict scope.
/// </summary>
public class OpenIddictScope : OpenIddictScope<string>
{
public OpenIddictScope()
{
// Generate a new string identifier.
Id = Guid.NewGuid().ToString();
}
}
/// <summary>
/// Represents an OpenIddict scope.
/// </summary>
public class OpenIddictScope<TKey> where TKey : IEquatable<TKey>
{
/// <summary>
/// Gets or sets the concurrency token.
/// </summary>
public virtual string ConcurrencyToken { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// Gets or sets the public description
/// associated with the current scope.
/// </summary>
public virtual string Description { get; set; }
/// <summary>
/// Gets or sets the display name
/// associated with the current scope.
/// </summary>
public virtual string DisplayName { get; set; }
/// <summary>
/// Gets or sets the unique identifier
/// associated with the current scope.
/// </summary>
public virtual TKey Id { get; set; }
/// <summary>
/// Gets or sets the unique name
/// associated with the current scope.
/// </summary>
public virtual string Name { get; set; }
/// <summary>
/// Gets or sets the additional properties serialized as a JSON object,
/// or <c>null</c> if no bag was associated with the current scope.
/// </summary>
public virtual string Properties { get; set; }
/// <summary>
/// Gets or sets the resources associated with the
/// current scope, serialized as a JSON array.
/// </summary>
public virtual string Resources { get; set; }
}
}

2
src/OpenIddict.Models/OpenIddictToken.cs → src/OpenIddict.EntityFrameworkCore.Models/OpenIddictToken.cs

@ -6,7 +6,7 @@
using System; using System;
namespace OpenIddict.Models namespace OpenIddict.EntityFrameworkCore.Models
{ {
/// <summary> /// <summary>
/// Represents an OpenIddict token. /// Represents an OpenIddict token.

2
src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj

@ -14,7 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" /> <ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.Stores\OpenIddict.Stores.csproj" /> <ProjectReference Include="..\OpenIddict.EntityFrameworkCore.Models\OpenIddict.EntityFrameworkCore.Models.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

123
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
{
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict Entity Framework Core services.
/// </summary>
public class OpenIddictEntityFrameworkCoreBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictEntityFrameworkCoreBuilder([NotNull] IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
Services = services;
}
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict Entity Framework Core configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
public OpenIddictEntityFrameworkCoreBuilder Configure([NotNull] Action<OpenIddictEntityFrameworkCoreOptions> configuration)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Configures the OpenIddict Entity Framework Core stores to use the specified database context type.
/// </summary>
/// <typeparam name="TContext">The type of the <see cref="DbContext"/> used by OpenIddict.</typeparam>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
public OpenIddictEntityFrameworkCoreBuilder UseDbContext<TContext>()
where TContext : DbContext => UseDbContext(typeof(TContext));
/// <summary>
/// Configures the OpenIddict Entity Framework Core stores to use the specified database context type.
/// </summary>
/// <param name="type">The type of the <see cref="DbContext"/> used by OpenIddict.</param>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
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);
}
/// <summary>
/// Configures OpenIddict to use the default OpenIddict
/// Entity Framework Core entities, with the specified key type.
/// </summary>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities<TKey>()
where TKey : IEquatable<TKey>
=> ReplaceDefaultEntities<OpenIddictApplication<TKey>,
OpenIddictAuthorization<TKey>,
OpenIddictScope<TKey>,
OpenIddictToken<TKey>, TKey>();
/// <summary>
/// Configures OpenIddict to use the specified entities, derived
/// from the default OpenIddict Entity Framework Core entities.
/// </summary>
/// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
public OpenIddictEntityFrameworkCoreBuilder ReplaceDefaultEntities<TApplication, TAuthorization, TScope, TToken, TKey>()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TScope : OpenIddictScope<TKey>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
Services.Configure<OpenIddictCoreOptions>(options =>
{
options.DefaultApplicationType = typeof(TApplication);
options.DefaultAuthorizationType = typeof(TAuthorization);
options.DefaultScopeType = typeof(TScope);
options.DefaultTokenType = typeof(TToken);
});
return this;
}
}
}

6
src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs → src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreCustomizer.cs

@ -9,7 +9,7 @@ using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
@ -17,14 +17,14 @@ namespace OpenIddict.EntityFrameworkCore
/// Represents a model customizer able to register the entity sets /// Represents a model customizer able to register the entity sets
/// required by the OpenIddict stack in an Entity Framework context. /// required by the OpenIddict stack in an Entity Framework context.
/// </summary> /// </summary>
public class OpenIddictCustomizer<TApplication, TAuthorization, TScope, TToken, TKey> : RelationalModelCustomizer public class OpenIddictEntityFrameworkCoreCustomizer<TApplication, TAuthorization, TScope, TToken, TKey> : RelationalModelCustomizer
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TScope : OpenIddictScope<TKey>, new() where TScope : OpenIddictScope<TKey>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictCustomizer([NotNull] ModelCustomizerDependencies dependencies) public OpenIddictEntityFrameworkCoreCustomizer([NotNull] ModelCustomizerDependencies dependencies)
: base(dependencies) : base(dependencies)
{ {
} }

61
src/OpenIddict.EntityFrameworkCore/OpenIddictExtensions.cs → src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs

@ -10,36 +10,69 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using OpenIddict.EntityFrameworkCore; using OpenIddict.EntityFrameworkCore;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace Microsoft.Extensions.DependencyInjection namespace Microsoft.Extensions.DependencyInjection
{ {
public static class OpenIddictExtensions public static class OpenIddictEntityFrameworkCoreExtensions
{ {
/// <summary> /// <summary>
/// Registers the Entity Framework Core stores. Note: when using the Entity Framework Core stores, /// Registers the Entity Framework Core stores services in the DI container and
/// the entities MUST be derived from the models contained in the OpenIddict.Models package. /// configures OpenIddict to use the Entity Framework Core entities by default.
/// </summary> /// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param> /// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks> /// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictEntityFrameworkCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder AddEntityFrameworkCoreStores<TContext>([NotNull] this OpenIddictCoreBuilder builder) public static OpenIddictEntityFrameworkCoreBuilder UseEntityFrameworkCore([NotNull] this OpenIddictCoreBuilder builder)
where TContext : DbContext
{ {
if (builder == null) if (builder == null)
{ {
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
builder.ReplaceApplicationStoreResolver<OpenIddictApplicationStoreResolver>()
.ReplaceAuthorizationStoreResolver<OpenIddictAuthorizationStoreResolver>()
.ReplaceScopeStoreResolver<OpenIddictScopeStoreResolver>()
.ReplaceTokenStoreResolver<OpenIddictTokenStoreResolver>();
builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictApplicationStore<,,,,>));
builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictAuthorizationStore<,,,,>));
builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeStore<,,>));
builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenStore<,,,,>));
return builder.ReplaceApplicationStoreResolver<OpenIddictApplicationStoreResolver<TContext>>() return new OpenIddictEntityFrameworkCoreBuilder(builder.Services);
.ReplaceAuthorizationStoreResolver<OpenIddictAuthorizationStoreResolver<TContext>>() }
.ReplaceScopeStoreResolver<OpenIddictScopeStoreResolver<TContext>>()
.ReplaceTokenStoreResolver<OpenIddictTokenStoreResolver<TContext>>(); /// <summary>
/// Registers the Entity Framework Core stores services in the DI container and
/// configures OpenIddict to use the Entity Framework Core entities by default.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the Entity Framework Core services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder UseEntityFrameworkCore(
[NotNull] this OpenIddictCoreBuilder builder,
[NotNull] Action<OpenIddictEntityFrameworkCoreBuilder> configuration)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseEntityFrameworkCore());
return builder;
} }
/// <summary> /// <summary>
@ -85,7 +118,8 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
return builder.ReplaceService<IModelCustomizer, OpenIddictCustomizer<TApplication, TAuthorization, TScope, TToken, TKey>>(); return builder.ReplaceService<IModelCustomizer, OpenIddictEntityFrameworkCoreCustomizer<
TApplication, TAuthorization, TScope, TToken, TKey>>();
} }
/// <summary> /// <summary>
@ -184,8 +218,7 @@ namespace Microsoft.Extensions.DependencyInjection
entity.HasMany(authorization => authorization.Tokens) entity.HasMany(authorization => authorization.Tokens)
.WithOne(token => token.Authorization) .WithOne(token => token.Authorization)
.HasForeignKey("AuthorizationId") .HasForeignKey("AuthorizationId")
.IsRequired(required: false) .IsRequired(required: false);
.OnDelete(DeleteBehavior.Cascade);
entity.ToTable("OpenIddictAuthorizations"); entity.ToTable("OpenIddictAuthorizations");
}); });

25
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
{
/// <summary>
/// Provides various settings needed to configure
/// the OpenIddict Entity Framework Core integration.
/// </summary>
public class OpenIddictEntityFrameworkCoreOptions
{
/// <summary>
/// Gets or sets the concrete type of the <see cref="DbContext"/> 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.
/// </summary>
public Type ContextType { get; set; }
}
}

39
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.Collections.Concurrent;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve an application store. /// Exposes a method allowing to resolve an application store.
/// </summary> /// </summary>
public class OpenIddictApplicationStoreResolver<TContext> : IOpenIddictApplicationStoreResolver public class OpenIddictApplicationStoreResolver : IOpenIddictApplicationStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictApplicationStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +54,19 @@ namespace OpenIddict.EntityFrameworkCore
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified application type is not compatible with the Entity Framework Core stores.") .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("When enabling the Entity Framework Core stores, make sure you use the built-in ")
.Append("'OpenIddictApplication' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictApplication' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ")
.Append("that inherits from the generic 'OpenIddictApplication' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +74,7 @@ namespace OpenIddict.EntityFrameworkCore
/* TApplication: */ key, /* TApplication: */ key,
/* TAuthorization: */ root.GenericTypeArguments[1], /* TAuthorization: */ root.GenericTypeArguments[1],
/* TToken: */ root.GenericTypeArguments[2], /* TToken: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

38
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.Collections.Concurrent;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve an authorization store. /// Exposes a method allowing to resolve an authorization store.
/// </summary> /// </summary>
public class OpenIddictAuthorizationStoreResolver<TContext> : IOpenIddictAuthorizationStoreResolver public class OpenIddictAuthorizationStoreResolver : IOpenIddictAuthorizationStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictAuthorizationStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +55,19 @@ namespace OpenIddict.EntityFrameworkCore
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified authorization type is not compatible with the Entity Framework Core stores.") .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("When enabling the Entity Framework Core stores, make sure you use the built-in ")
.Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictAuthorization' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ")
.Append("that inherits from the generic 'OpenIddictAuthorization' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +75,7 @@ namespace OpenIddict.EntityFrameworkCore
/* TAuthorization: */ key, /* TAuthorization: */ key,
/* TApplication: */ root.GenericTypeArguments[1], /* TApplication: */ root.GenericTypeArguments[1],
/* TToken: */ root.GenericTypeArguments[2], /* TToken: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

39
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.Collections.Concurrent;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve a scope store. /// Exposes a method allowing to resolve a scope store.
/// </summary> /// </summary>
public class OpenIddictScopeStoreResolver<TContext> : IOpenIddictScopeStoreResolver public class OpenIddictScopeStoreResolver : IOpenIddictScopeStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) public OpenIddictScopeStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,15 +54,25 @@ namespace OpenIddict.EntityFrameworkCore
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified scope type is not compatible with the Entity Framework Core stores.") .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("When enabling the Entity Framework Core stores, make sure you use the built-in ")
.Append("'OpenIdScope' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIdScope' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ")
.Append("that inherits from the generic 'OpenIddictScope' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
return typeof(OpenIddictScopeStore<,,>).MakeGenericType( return typeof(OpenIddictScopeStore<,,>).MakeGenericType(
/* TScope: */ key, /* TScope: */ key,
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

39
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.Collections.Concurrent;
using System.Text; using System.Text;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Exposes a method allowing to resolve a token store. /// Exposes a method allowing to resolve a token store.
/// </summary> /// </summary>
public class OpenIddictTokenStoreResolver<TContext> : IOpenIddictTokenStoreResolver public class OpenIddictTokenStoreResolver : IOpenIddictTokenStoreResolver
where TContext : DbContext
{ {
private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>(); private static readonly ConcurrentDictionary<Type, Type> _cache = new ConcurrentDictionary<Type, Type>();
private readonly IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> _options;
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) public OpenIddictTokenStoreResolver(
[NotNull] IOptionsMonitor<OpenIddictEntityFrameworkCoreOptions> options,
[NotNull] IServiceProvider provider)
{ {
_options = options;
_provider = provider; _provider = provider;
} }
@ -45,9 +54,19 @@ namespace OpenIddict.EntityFrameworkCore
{ {
throw new InvalidOperationException(new StringBuilder() throw new InvalidOperationException(new StringBuilder()
.AppendLine("The specified token type is not compatible with the Entity Framework Core stores.") .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("When enabling the Entity Framework Core stores, make sure you use the built-in ")
.Append("'OpenIddictToken' entity (from the 'OpenIddict.Models' package) or a custom entity ") .Append("'OpenIddictToken' entity (from the 'OpenIddict.EntityFrameworkCore.Models' package) ")
.Append("that inherits from the generic 'OpenIddictToken' entity.") .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<TContext>()'.")
.ToString()); .ToString());
} }
@ -55,7 +74,7 @@ namespace OpenIddict.EntityFrameworkCore
/* TToken: */ key, /* TToken: */ key,
/* TApplication: */ root.GenericTypeArguments[1], /* TApplication: */ root.GenericTypeArguments[1],
/* TAuthorization: */ root.GenericTypeArguments[2], /* TAuthorization: */ root.GenericTypeArguments[2],
/* TContext: */ typeof(TContext), /* TContext: */ context,
/* TKey: */ root.GenericTypeArguments[0]); /* TKey: */ root.GenericTypeArguments[0]);
}); });

649
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -7,8 +7,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -16,14 +18,15 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using OpenIddict.Models; using Newtonsoft.Json;
using OpenIddict.Stores; using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using OpenIddict.EntityFrameworkCore.Models;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in a database. /// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictApplicationStore<TContext> : OpenIddictApplicationStore<OpenIddictApplication, public class OpenIddictApplicationStore<TContext> : OpenIddictApplicationStore<OpenIddictApplication,
@ -31,15 +34,14 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictToken, TContext, string> OpenIddictToken, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in a database. /// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
@ -49,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the applications stored in a database. /// Provides methods allowing to manage the applications stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TContext, TKey> : public class OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TContext, TKey> : IOpenIddictApplicationStore<TApplication>
OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TKey>
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictApplicationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictApplicationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of applications that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Applications.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of applications that match the specified query. /// Determines the number of applications that match the specified query.
@ -113,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query. /// whose result returns the number of applications that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -131,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -151,7 +162,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -229,8 +240,19 @@ namespace OpenIddict.EntityFrameworkCore
Context.Remove(application); Context.Remove(application);
await Context.SaveChangesAsync(cancellationToken); try
transaction?.Commit(); {
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 <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
public override Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -272,7 +294,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
public override Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -301,7 +323,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri. /// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -354,7 +376,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri. /// returns the client applications corresponding to the specified redirect_uri.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -410,7 +432,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -422,6 +444,296 @@ namespace OpenIddict.EntityFrameworkCore
return query(Applications.AsTracking(), state).FirstOrDefaultAsync(cancellationToken); return query(Applications.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
} }
/// <summary>
/// Retrieves the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientId);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client secret associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientSecret);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
/// </returns>
public virtual ValueTask<string> GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.Type);
}
/// <summary>
/// Retrieves the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
/// </returns>
public virtual ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ConsentType);
}
/// <summary>
/// Retrieves the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(ConvertIdentifierToString(application.Id));
}
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Permissions))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(permissions);
}
/// <summary>
/// Retrieves the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.PostLogoutRedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the application.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(application.Properties));
}
/// <summary>
/// Retrieves the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.RedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated application, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TApplication> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TApplication>(new TApplication());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -434,7 +746,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -446,6 +758,232 @@ namespace OpenIddict.EntityFrameworkCore
return ImmutableArray.CreateRange(await query(Applications.AsTracking(), state).ToListAsync(cancellationToken)); return ImmutableArray.CreateRange(await query(Applications.AsTracking(), state).ToListAsync(cancellationToken));
} }
/// <summary>
/// Sets the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The client secret associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The client type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The consent type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="name">The display name associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="permissions">The permissions associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="properties">The additional properties associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary> /// <summary>
/// Updates an existing application. /// Updates an existing application.
/// </summary> /// </summary>
@ -454,7 +992,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -469,7 +1007,48 @@ namespace OpenIddict.EntityFrameworkCore
Context.Update(application); 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

447
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

@ -7,8 +7,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -16,15 +18,15 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Stores;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in a database. /// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictAuthorizationStore<TContext> : OpenIddictAuthorizationStore<OpenIddictAuthorization, public class OpenIddictAuthorizationStore<TContext> : OpenIddictAuthorizationStore<OpenIddictAuthorization,
@ -32,15 +34,14 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictToken, TContext, string> OpenIddictToken, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in a database. /// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
@ -50,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the authorizations stored in a database. /// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TContext, TKey> : public class OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TContext, TKey> : IOpenIddictAuthorizationStore<TAuthorization>
OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TKey>
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictAuthorizationStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictAuthorizationStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of authorizations that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Authorizations.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of authorizations that match the specified query. /// Determines the number of authorizations that match the specified query.
@ -114,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query. /// whose result returns the number of authorizations that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -132,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -152,7 +162,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -206,8 +216,19 @@ namespace OpenIddict.EntityFrameworkCore
Context.Remove(authorization); Context.Remove(authorization);
await Context.SaveChangesAsync(cancellationToken); try
transaction?.Commit(); {
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 <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client. /// whose result returns the authorizations corresponding to the subject/client.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TAuthorization>> FindAsync( public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken) [NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(subject)) if (string.IsNullOrEmpty(subject))
@ -268,7 +289,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria. /// whose result returns the authorizations corresponding to the criteria.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TAuthorization>> FindAsync( public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, [NotNull] string subject, [NotNull] string client,
[NotNull] string status, CancellationToken cancellationToken) [NotNull] string status, CancellationToken cancellationToken)
{ {
@ -321,7 +342,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria. /// whose result returns the authorizations corresponding to the criteria.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TAuthorization>> FindAsync( public virtual async Task<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, [NotNull] string subject, [NotNull] string client,
[NotNull] string status, [NotNull] string type, CancellationToken cancellationToken) [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken)
{ {
@ -378,7 +399,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier. /// whose result returns the authorization corresponding to the identifier.
/// </returns> /// </returns>
public override Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -409,7 +430,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject. /// whose result returns the authorizations corresponding to the specified subject.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TAuthorization>> FindBySubjectAsync( public virtual async Task<ImmutableArray<TAuthorization>> FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken) [NotNull] string subject, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(subject)) if (string.IsNullOrEmpty(subject))
@ -441,7 +462,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the authorization. /// whose result returns the application identifier associated with the authorization.
/// </returns> /// </returns>
public override async ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -480,7 +501,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -494,6 +515,171 @@ namespace OpenIddict.EntityFrameworkCore
.AsTracking(), state).FirstOrDefaultAsync(cancellationToken); .AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
} }
/// <summary>
/// Retrieves the unique identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(ConvertIdentifierToString(authorization.Id));
}
/// <summary>
/// Retrieves the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the authorization.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(authorization.Properties));
}
/// <summary>
/// Retrieves the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Scopes))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray());
}
/// <summary>
/// Retrieves the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Status);
}
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Subject);
}
/// <summary>
/// Retrieves the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Type);
}
/// <summary>
/// Instantiates a new authorization.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated authorization, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TAuthorization> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TAuthorization>(new TAuthorization());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TAuthorization>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -506,7 +692,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -527,7 +713,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
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 // 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. // entities in a single command without having to retrieve and materialize them first.
@ -535,14 +721,6 @@ namespace OpenIddict.EntityFrameworkCore
IList<Exception> exceptions = null; IList<Exception> exceptions = null;
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> 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<IDbContextTransaction> CreateTransactionAsync() async Task<IDbContextTransaction> CreateTransactionAsync()
{ {
// Note: transactions that specify an explicit isolation level are only supported by // 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. // and thus prevent them from being concurrently modified outside this block.
using (var transaction = await CreateTransactionAsync()) using (var transaction = await CreateTransactionAsync())
{ {
var authorizations = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); var authorizations =
if (authorizations.IsEmpty) 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; break;
} }
@ -625,7 +810,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetApplicationIdAsync([NotNull] TAuthorization authorization, public virtual async Task SetApplicationIdAsync([NotNull] TAuthorization authorization,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
@ -662,6 +847,129 @@ namespace OpenIddict.EntityFrameworkCore
} }
} }
/// <summary>
/// Sets the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="properties">The additional properties associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="scopes">The scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetScopesAsync([NotNull] TAuthorization authorization,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="type">The type associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary> /// <summary>
/// Updates an existing authorization. /// Updates an existing authorization.
/// </summary> /// </summary>
@ -670,7 +978,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -685,7 +993,48 @@ namespace OpenIddict.EntityFrameworkCore
Context.Update(authorization); 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

420
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs

@ -6,33 +6,36 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; 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 namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in a database. /// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictScopeStore<TContext> : OpenIddictScopeStore<OpenIddictScope, TContext, string> public class OpenIddictScopeStore<TContext> : OpenIddictScopeStore<OpenIddictScope, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in a database. /// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
@ -40,44 +43,54 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the scopes stored in a database. /// Provides methods allowing to manage the scopes stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam> /// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictScopeStore<TScope, TContext, TKey> : Stores.OpenIddictScopeStore<TScope, TKey> public class OpenIddictScopeStore<TScope, TContext, TKey> : IOpenIddictScopeStore<TScope>
where TScope : OpenIddictScope<TKey>, new() where TScope : OpenIddictScope<TKey>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictScopeStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictScopeStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TScope"/> entity. /// Gets the database set corresponding to the <typeparamref name="TScope"/> entity.
/// </summary> /// </summary>
protected DbSet<TScope> Scopes => Context.Set<TScope>(); private DbSet<TScope> Scopes => Context.Set<TScope>();
/// <summary>
/// Determines the number of scopes that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Scopes.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of scopes that match the specified query. /// Determines the number of scopes that match the specified query.
@ -89,7 +102,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query. /// whose result returns the number of scopes that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -107,7 +120,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -127,7 +140,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -136,7 +149,18 @@ namespace OpenIddict.EntityFrameworkCore
Context.Remove(scope); 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);
}
} }
/// <summary> /// <summary>
@ -148,7 +172,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier. /// whose result returns the scope corresponding to the identifier.
/// </returns> /// </returns>
public override Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -177,7 +201,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name. /// whose result returns the scope corresponding to the specified name.
/// </returns> /// </returns>
public override Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) public virtual Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(name)) if (string.IsNullOrEmpty(name))
{ {
@ -206,7 +230,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names. /// whose result returns the scopes corresponding to the specified names.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TScope>> FindByNamesAsync( public virtual async Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken) ImmutableArray<string> names, CancellationToken cancellationToken)
{ {
if (names.Any(name => string.IsNullOrEmpty(name))) if (names.Any(name => string.IsNullOrEmpty(name)))
@ -236,7 +260,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource. /// whose result returns the scopes associated with the specified resource.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TScope>> FindByResourceAsync( public virtual async Task<ImmutableArray<TScope>> FindByResourceAsync(
[NotNull] string resource, CancellationToken cancellationToken) [NotNull] string resource, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(resource)) if (string.IsNullOrEmpty(resource))
@ -285,7 +309,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -297,6 +321,182 @@ namespace OpenIddict.EntityFrameworkCore
return query(Scopes.AsTracking(), state).FirstOrDefaultAsync(cancellationToken); return query(Scopes.AsTracking(), state).FirstOrDefaultAsync(cancellationToken);
} }
/// <summary>
/// Retrieves the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the description associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Description);
}
/// <summary>
/// Retrieves the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the scope.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the scope.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(ConvertIdentifierToString(scope.Id));
}
/// <summary>
/// Retrieves the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the name associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Name);
}
/// <summary>
/// Retrieves the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the scope.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(scope.Properties));
}
/// <summary>
/// Retrieves the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the resources associated with the scope.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Resources))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(resources);
}
/// <summary>
/// Instantiates a new scope.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated scope, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TScope> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TScope>(new TScope());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -309,7 +509,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -321,6 +521,125 @@ namespace OpenIddict.EntityFrameworkCore
return ImmutableArray.CreateRange(await query(Scopes.AsTracking(), state).ToListAsync(cancellationToken)); return ImmutableArray.CreateRange(await query(Scopes.AsTracking(), state).ToListAsync(cancellationToken));
} }
/// <summary>
/// Sets the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="description">The description associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The display name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The name associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="properties">The additional properties associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="resources">The resources associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray<string> 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;
}
/// <summary> /// <summary>
/// Updates an existing scope. /// Updates an existing scope.
/// </summary> /// </summary>
@ -329,7 +648,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{ {
if (scope == null) if (scope == null)
{ {
@ -344,7 +663,48 @@ namespace OpenIddict.EntityFrameworkCore
Context.Update(scope); 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

578
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

@ -7,8 +7,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
@ -16,16 +18,15 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.EntityFrameworkCore.Models;
using OpenIddict.Models;
using OpenIddict.Stores;
namespace OpenIddict.EntityFrameworkCore namespace OpenIddict.EntityFrameworkCore
{ {
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in a database. /// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
public class OpenIddictTokenStore<TContext> : OpenIddictTokenStore<OpenIddictToken, public class OpenIddictTokenStore<TContext> : OpenIddictTokenStore<OpenIddictToken,
@ -33,15 +34,14 @@ namespace OpenIddict.EntityFrameworkCore
OpenIddictAuthorization, TContext, string> OpenIddictAuthorization, TContext, string>
where TContext : DbContext where TContext : DbContext
{ {
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in a database. /// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
@ -51,59 +51,68 @@ namespace OpenIddict.EntityFrameworkCore
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(context, cache) : base(cache, context)
{ {
} }
} }
/// <summary> /// <summary>
/// Provides methods allowing to manage the tokens stored in a database. /// Provides methods allowing to manage the tokens stored in a database.
/// Note: this class can only be used with the default OpenIddict entities.
/// </summary> /// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam> /// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam> /// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam> /// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam> /// <typeparam name="TContext">The type of the Entity Framework database context.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam> /// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public class OpenIddictTokenStore<TToken, TApplication, TAuthorization, TContext, TKey> : public class OpenIddictTokenStore<TToken, TApplication, TAuthorization, TContext, TKey> : IOpenIddictTokenStore<TToken>
OpenIddictTokenStore<TToken, TApplication, TAuthorization, TKey>
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new() where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new() where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new() where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TContext : DbContext where TContext : DbContext
where TKey : IEquatable<TKey> where TKey : IEquatable<TKey>
{ {
public OpenIddictTokenStore([NotNull] TContext context, [NotNull] IMemoryCache cache) public OpenIddictTokenStore([NotNull] IMemoryCache cache, [NotNull] TContext context)
: base(cache)
{ {
if (context == null) Cache = cache;
{
throw new ArgumentNullException(nameof(context));
}
Context = context; Context = context;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
protected virtual TContext Context { get; } protected TContext Context { get; }
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity. /// Gets the database set corresponding to the <typeparamref name="TApplication"/> entity.
/// </summary> /// </summary>
protected DbSet<TApplication> Applications => Context.Set<TApplication>(); private DbSet<TApplication> Applications => Context.Set<TApplication>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity. /// Gets the database set corresponding to the <typeparamref name="TAuthorization"/> entity.
/// </summary> /// </summary>
protected DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>(); private DbSet<TAuthorization> Authorizations => Context.Set<TAuthorization>();
/// <summary> /// <summary>
/// Gets the database set corresponding to the <typeparamref name="TToken"/> entity. /// Gets the database set corresponding to the <typeparamref name="TToken"/> entity.
/// </summary> /// </summary>
protected DbSet<TToken> Tokens => Context.Set<TToken>(); private DbSet<TToken> Tokens => Context.Set<TToken>();
/// <summary>
/// Determines the number of tokens that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
=> Tokens.LongCountAsync();
/// <summary> /// <summary>
/// Determines the number of tokens that match the specified query. /// Determines the number of tokens that match the specified query.
@ -115,7 +124,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query. /// whose result returns the number of tokens that match the specified query.
/// </returns> /// </returns>
public override Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -133,7 +142,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -150,8 +159,10 @@ namespace OpenIddict.EntityFrameworkCore
/// </summary> /// </summary>
/// <param name="token">The token to delete.</param> /// <param name="token">The token to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns> /// <returns>
public override Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken) /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -160,7 +171,18 @@ namespace OpenIddict.EntityFrameworkCore
Context.Remove(token); 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);
}
} }
/// <summary> /// <summary>
@ -172,7 +194,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application. /// whose result returns the tokens corresponding to the specified application.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -210,7 +232,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization. /// whose result returns the tokens corresponding to the specified authorization.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -248,7 +270,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier. /// whose result returns the token corresponding to the unique identifier.
/// </returns> /// </returns>
public override Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -281,7 +303,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier. /// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns> /// </returns>
public override Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -313,7 +335,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject. /// whose result returns the tokens corresponding to the specified subject.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TToken>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TToken>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(subject)) if (string.IsNullOrEmpty(subject))
{ {
@ -345,7 +367,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the token. /// whose result returns the application identifier associated with the token.
/// </returns> /// </returns>
public override async ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -384,7 +406,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query. /// whose result returns the first element returned when executing the query.
/// </returns> /// </returns>
public override Task<TResult> GetAsync<TState, TResult>( public virtual Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -408,7 +430,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the token. /// whose result returns the authorization identifier associated with the token.
/// </returns> /// </returns>
public override async ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -435,6 +457,226 @@ namespace OpenIddict.EntityFrameworkCore
return ConvertIdentifierToString(token.Authorization.Id); return ConvertIdentifierToString(token.Authorization.Id);
} }
/// <summary>
/// Retrieves the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.CreationDate);
}
/// <summary>
/// Retrieves the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the expiration date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.ExpirationDate);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(ConvertIdentifierToString(token.Id));
}
/// <summary>
/// Retrieves the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the payload associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Payload);
}
/// <summary>
/// Retrieves the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the token.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(token.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(token.Properties));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the reference identifier associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.ReferenceId);
}
/// <summary>
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Status);
}
/// <summary>
/// Retrieves the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Subject);
}
/// <summary>
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Type);
}
/// <summary>
/// Instantiates a new token.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated token, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TToken> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TToken>(new TToken());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual async Task<ImmutableArray<TToken>> 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));
}
/// <summary> /// <summary>
/// Executes the specified query and returns all the corresponding elements. /// Executes the specified query and returns all the corresponding elements.
/// </summary> /// </summary>
@ -447,7 +689,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query. /// whose result returns all the elements returned when executing the specified query.
/// </returns> /// </returns>
public override async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>( public virtual async Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query, [NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken) [CanBeNull] TState state, CancellationToken cancellationToken)
{ {
@ -469,7 +711,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
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 // 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. // entities in a single command without having to retrieve and materialize them first.
@ -477,13 +719,6 @@ namespace OpenIddict.EntityFrameworkCore
IList<Exception> exceptions = null; IList<Exception> exceptions = null;
IQueryable<TToken> Query(IQueryable<TToken> 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<IDbContextTransaction> CreateTransactionAsync() async Task<IDbContextTransaction> CreateTransactionAsync()
{ {
// Note: transactions that specify an explicit isolation level are only supported by // 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. // and thus prevent them from being concurrently modified outside this block.
using (var transaction = await CreateTransactionAsync()) using (var transaction = await CreateTransactionAsync())
{ {
var tokens = await ListAsync((source, state) => Query(source, state), offset, cancellationToken); var tokens =
if (tokens.IsEmpty) 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; break;
} }
@ -561,7 +802,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetApplicationIdAsync([NotNull] TToken token, public virtual async Task SetApplicationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
@ -607,7 +848,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override async Task SetAuthorizationIdAsync([NotNull] TToken token, public virtual async Task SetAuthorizationIdAsync([NotNull] TToken token,
[CanBeNull] string identifier, CancellationToken cancellationToken) [CanBeNull] string identifier, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
@ -644,6 +885,200 @@ namespace OpenIddict.EntityFrameworkCore
} }
} }
/// <summary>
/// Sets the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The creation date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The expiration date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="payload">The payload associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="properties">The additional properties associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The reference identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The token type associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary> /// <summary>
/// Updates an existing token. /// Updates an existing token.
/// </summary> /// </summary>
@ -652,7 +1087,7 @@ namespace OpenIddict.EntityFrameworkCore
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public override Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual async Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {
@ -667,7 +1102,48 @@ namespace OpenIddict.EntityFrameworkCore
Context.Update(token); 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);
}
}
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
} }
} }
} }

44
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs

@ -391,13 +391,21 @@ namespace OpenIddict.Server
return true; 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); "associated with the token '{Identifier}'.", identifier);
return false; 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<bool> TryRevokeTokenAsync([NotNull] object token) private async Task<bool> TryRevokeTokenAsync([NotNull] object token)
@ -416,10 +424,16 @@ namespace OpenIddict.Server
return true; 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) catch (Exception exception)
{ {
_logger.LogDebug(exception, "An exception occurred while trying to revoke " + _logger.LogWarning(exception, "An exception occurred while trying to revoke the token '{Identifier}'.", identifier);
"the token '{Identifier}'.", identifier);
return false; return false;
} }
@ -466,10 +480,16 @@ namespace OpenIddict.Server
return true; 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) catch (Exception exception)
{ {
_logger.LogDebug(exception, "An exception occurred while trying to " + _logger.LogWarning(exception, "An exception occurred while trying to redeem the token '{Identifier}'.", identifier);
"redeem the token '{Identifier}'.", identifier);
return false; return false;
} }
@ -497,13 +517,21 @@ namespace OpenIddict.Server
return true; 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); "expiration date of the token '{Identifier}'.", identifier);
return false; 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( private IEnumerable<(string property, string parameter, OpenIdConnectParameter value)> GetParameters(

10
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -59,11 +59,6 @@ namespace OpenIddict.Server
OpenIdConnectConstants.Claims.Subject OpenIdConnectConstants.Claims.Subject
}; };
/// <summary>
/// Gets or sets a boolean indicating whether scope validation is enabled.
/// </summary>
public bool EnableScopeValidation { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether token revocation should be disabled. /// Gets or sets a boolean indicating whether token revocation should be disabled.
/// When disabled, authorization code and refresh tokens are not stored /// When disabled, authorization code and refresh tokens are not stored
@ -80,6 +75,11 @@ namespace OpenIddict.Server
/// </summary> /// </summary>
public bool EnableRequestCaching { get; set; } public bool EnableRequestCaching { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether scope validation is enabled.
/// </summary>
public bool EnableScopeValidation { get; set; }
/// <summary> /// <summary>
/// Gets the OAuth2/OpenID Connect flows enabled for this application. /// Gets the OAuth2/OpenID Connect flows enabled for this application.
/// </summary> /// </summary>

25
src/OpenIddict.Stores/OpenIddict.Stores.csproj

@ -1,25 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\packages.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<Description>Default base stores for OpenIddict.</Description>
<Authors>Kévin Chalet</Authors>
<PackageTags>aspnetcore;authentication;jwt;openidconnect;openiddict;security</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
<ProjectReference Include="..\OpenIddict.Models\OpenIddict.Models.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="$(AspNetCoreVersion)" />
</ItemGroup>
</Project>

832
src/OpenIddict.Stores/Stores/OpenIddictApplicationStore.cs

@ -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
{
/// <summary>
/// Provides methods allowing to manage the applications stored in a database.
/// Note: this base class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public abstract class OpenIddictApplicationStore<TApplication, TAuthorization, TToken, TKey> : IOpenIddictApplicationStore<TApplication>
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
protected OpenIddictApplicationStore([NotNull] IMemoryCache cache)
{
if (cache == null)
{
throw new ArgumentNullException(nameof(cache));
}
Cache = cache;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Determines the number of applications that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
{
return CountAsync(applications => applications, cancellationToken);
}
/// <summary>
/// Determines the number of applications that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query.
/// </returns>
public abstract Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken);
/// <summary>
/// Creates a new application.
/// </summary>
/// <param name="application">The application to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Removes an existing application.
/// </summary>
/// <param name="application">The application to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TApplication> Query(IQueryable<TApplication> applications, TKey key)
=> from application in applications
where application.Id.Equals(key)
select application;
return GetAsync((applications, key) => Query(applications, key), ConvertIdentifierFromString(identifier), cancellationToken);
}
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public virtual Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TApplication> Query(IQueryable<TApplication> applications, string id)
=> from application in applications
where application.ClientId == id
select application;
return GetAsync((applications, id) => Query(applications, id), identifier, cancellationToken);
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication> Query(IQueryable<TApplication> applications, string uri)
=> from application in applications
where application.PostLogoutRedirectUris.Contains(uri)
select application;
var builder = ImmutableArray.CreateBuilder<TApplication>();
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();
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public virtual async Task<ImmutableArray<TApplication>> 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<TApplication> Query(IQueryable<TApplication> applications, string uri)
=> from application in applications
where application.RedirectUris.Contains(uri)
select application;
var builder = ImmutableArray.CreateBuilder<TApplication>();
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();
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public abstract Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientId);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client secret associated with the application.
/// </returns>
public virtual ValueTask<string> GetClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ClientSecret);
}
/// <summary>
/// Retrieves the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client type of the application (by default, "public").
/// </returns>
public virtual ValueTask<string> GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.Type);
}
/// <summary>
/// Retrieves the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit").
/// </returns>
public virtual ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.ConsentType);
}
/// <summary>
/// Retrieves the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(application.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the application.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
return new ValueTask<string>(ConvertIdentifierToString(application.Id));
}
/// <summary>
/// Retrieves the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Permissions))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(permissions);
}
/// <summary>
/// Retrieves the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.PostLogoutRedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Retrieves the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the application.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(application.Properties));
}
/// <summary>
/// Retrieves the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken)
{
if (application == null)
{
throw new ArgumentNullException(nameof(application));
}
if (string.IsNullOrEmpty(application.RedirectUris))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(addresses);
}
/// <summary>
/// Instantiates a new application.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated application, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TApplication> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TApplication>(new TApplication());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TApplication>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
IQueryable<TApplication> Query(IQueryable<TApplication> 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);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public abstract Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TApplication>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Sets the client identifier associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="secret">The client secret associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the client type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The client type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the consent type associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="type">The consent type associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="name">The display name associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the permissions associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="permissions">The permissions associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPermissionsAsync([NotNull] TApplication application, ImmutableArray<string> 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;
}
/// <summary>
/// Sets the logout callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The logout callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetPostLogoutRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the additional properties associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="properties">The additional properties associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the callback addresses associated with an application.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="addresses">The callback addresses associated with the application </param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetRedirectUrisAsync([NotNull] TApplication application,
ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing application.
/// </summary>
/// <param name="application">The application to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
}
}
}

694
src/OpenIddict.Stores/Stores/OpenIddictAuthorizationStore.cs

@ -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
{
/// <summary>
/// Provides methods allowing to manage the authorizations stored in a database.
/// Note: this base class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public abstract class OpenIddictAuthorizationStore<TAuthorization, TApplication, TToken, TKey> : IOpenIddictAuthorizationStore<TAuthorization>
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TKey : IEquatable<TKey>
{
protected OpenIddictAuthorizationStore([NotNull] IMemoryCache cache)
{
if (cache == null)
{
throw new ArgumentNullException(nameof(cache));
}
Cache = cache;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Determines the number of authorizations that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
{
return CountAsync(authorizations => authorizations, cancellationToken);
}
/// <summary>
/// Determines the number of authorizations that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query.
/// </returns>
public abstract Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken);
/// <summary>
/// Creates a new authorization.
/// </summary>
/// <param name="authorization">The authorization to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task CreateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Removes an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> 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<TAuthorization> Query(IQueryable<TAuthorization> 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);
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> 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<TAuthorization> Query(IQueryable<TAuthorization> 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);
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> 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<TAuthorization> Query(IQueryable<TAuthorization> 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);
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public virtual Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations, TKey key)
=> from authorization in authorizations
where authorization.Id.Equals(key)
select authorization;
return GetAsync((authorizations, key) => Query(authorizations, key), ConvertIdentifierFromString(identifier), cancellationToken);
}
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindBySubjectAsync(
[NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> authorizations, string principal)
=> from authorization in authorizations
where authorization.Subject == principal
select authorization;
return ListAsync((authorizations, principal) => Query(authorizations, principal), subject, cancellationToken);
}
/// <summary>
/// Retrieves the optional application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetApplicationIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (authorization.Application != null)
{
return new ValueTask<string>(ConvertIdentifierToString(authorization.Application.Id));
}
async Task<string> RetrieveApplicationIdAsync()
{
IQueryable<TKey> Query(IQueryable<TAuthorization> 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<string>(RetrieveApplicationIdAsync());
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public abstract Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the unique identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the authorization.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(ConvertIdentifierToString(authorization.Id));
}
/// <summary>
/// Retrieves the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the authorization.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(authorization.Properties));
}
/// <summary>
/// Retrieves the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
if (string.IsNullOrEmpty(authorization.Scopes))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
return new ValueTask<ImmutableArray<string>>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray());
}
/// <summary>
/// Retrieves the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Status);
}
/// <summary>
/// Retrieves the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Subject);
}
/// <summary>
/// Retrieves the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization.
/// </returns>
public virtual ValueTask<string> GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
return new ValueTask<string>(authorization.Type);
}
/// <summary>
/// Instantiates a new authorization.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated authorization, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TAuthorization> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TAuthorization>(new TAuthorization());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TAuthorization>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
IQueryable<TAuthorization> Query(IQueryable<TAuthorization> 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);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public abstract Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TAuthorization>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task PruneAsync(CancellationToken cancellationToken);
/// <summary>
/// Sets the application identifier associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="identifier">The unique identifier associated with the client application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task SetApplicationIdAsync([NotNull] TAuthorization authorization,
[CanBeNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the additional properties associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="properties">The additional properties associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the scopes associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="scopes">The scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetScopesAsync([NotNull] TAuthorization authorization,
ImmutableArray<string> 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;
}
/// <summary>
/// Sets the status associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the type associated with an authorization.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="type">The type associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Updates an existing authorization.
/// </summary>
/// <param name="authorization">The authorization to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task UpdateAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
}
}
}

577
src/OpenIddict.Stores/Stores/OpenIddictScopeStore.cs

@ -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
{
/// <summary>
/// Provides methods allowing to manage the scopes stored in a database.
/// Note: this base class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public abstract class OpenIddictScopeStore<TScope, TKey> : IOpenIddictScopeStore<TScope>
where TScope : OpenIddictScope<TKey>, new()
where TKey : IEquatable<TKey>
{
protected OpenIddictScopeStore([NotNull] IMemoryCache cache)
{
if (cache == null)
{
throw new ArgumentNullException(nameof(cache));
}
Cache = cache;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Determines the number of scopes that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
{
return CountAsync(scopes => scopes, cancellationToken);
}
/// <summary>
/// Determines the number of scopes that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query.
/// </returns>
public abstract Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken);
/// <summary>
/// Creates a new scope.
/// </summary>
/// <param name="scope">The scope to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task CreateAsync([NotNull] TScope scope, CancellationToken cancellationToken);
/// <summary>
/// Removes an existing scope.
/// </summary>
/// <param name="scope">The scope to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public virtual Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TScope> Query(IQueryable<TScope> scopes, TKey key)
=> from scope in scopes
where scope.Id.Equals(key)
select scope;
return GetAsync((scopes, key) => Query(scopes, key), ConvertIdentifierFromString(identifier), cancellationToken);
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public virtual Task<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name));
}
IQueryable<TScope> Query(IQueryable<TScope> scopes, string state)
=> from scope in scopes
where scope.Name == state
select scope;
return GetAsync((scopes, state) => Query(scopes, state), name, cancellationToken);
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public virtual Task<ImmutableArray<TScope>> FindByNamesAsync(
ImmutableArray<string> names, CancellationToken cancellationToken)
{
if (names.Any(name => string.IsNullOrEmpty(name)))
{
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names));
}
IQueryable<TScope> Query(IQueryable<TScope> scopes, string[] values)
=> from scope in scopes
where values.Contains(scope.Name)
select scope;
return ListAsync((scopes, values) => Query(scopes, values), names.ToArray(), cancellationToken);
}
/// <summary>
/// Retrieves all the scopes that contain the specified resource.
/// </summary>
/// <param name="resource">The resource associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource.
/// </returns>
public virtual async Task<ImmutableArray<TScope>> 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<TScope> Query(IQueryable<TScope> scopes, string state)
=> from scope in scopes
where scope.Resources.Contains(state)
select scope;
var builder = ImmutableArray.CreateBuilder<TScope>();
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();
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public abstract Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the description associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetDescriptionAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Description);
}
/// <summary>
/// Retrieves the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the scope.
/// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.DisplayName);
}
/// <summary>
/// Retrieves the unique identifier associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the scope.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(ConvertIdentifierToString(scope.Id));
}
/// <summary>
/// Retrieves the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the name associated with the specified scope.
/// </returns>
public virtual ValueTask<string> GetNameAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
return new ValueTask<string>(scope.Name);
}
/// <summary>
/// Retrieves the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the scope.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(scope.Properties));
}
/// <summary>
/// Retrieves the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the resources associated with the scope.
/// </returns>
public virtual ValueTask<ImmutableArray<string>> GetResourcesAsync([NotNull] TScope scope, CancellationToken cancellationToken)
{
if (scope == null)
{
throw new ArgumentNullException(nameof(scope));
}
if (string.IsNullOrEmpty(scope.Resources))
{
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
}
// 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<ImmutableArray<string>>(resources);
}
/// <summary>
/// Instantiates a new scope.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated scope, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TScope> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TScope>(new TScope());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TScope>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
IQueryable<TScope> Query(IQueryable<TScope> 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);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public abstract Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TScope>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Sets the description associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="description">The description associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the display name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The display name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the name associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="name">The name associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="properties">The additional properties associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the resources associated with a scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="resources">The resources associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task SetResourcesAsync([NotNull] TScope scope, ImmutableArray<string> 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;
}
/// <summary>
/// Updates an existing scope.
/// </summary>
/// <param name="scope">The scope to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task UpdateAsync([NotNull] TScope scope, CancellationToken cancellationToken);
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
}
}
}

805
src/OpenIddict.Stores/Stores/OpenIddictTokenStore.cs

@ -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
{
/// <summary>
/// Provides methods allowing to manage the tokens stored in a database.
/// Note: this base class can only be used with the default OpenIddict entities.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
public abstract class OpenIddictTokenStore<TToken, TApplication, TAuthorization, TKey> : IOpenIddictTokenStore<TToken>
where TToken : OpenIddictToken<TKey, TApplication, TAuthorization>, new()
where TApplication : OpenIddictApplication<TKey, TAuthorization, TToken>, new()
where TAuthorization : OpenIddictAuthorization<TKey, TApplication, TToken>, new()
where TKey : IEquatable<TKey>
{
protected OpenIddictTokenStore([NotNull] IMemoryCache cache)
{
if (cache == null)
{
throw new ArgumentNullException(nameof(cache));
}
Cache = cache;
}
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary>
/// Determines the number of tokens that exist in the database.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications in the database.
/// </returns>
public virtual Task<long> CountAsync(CancellationToken cancellationToken)
{
return CountAsync(tokens => tokens, cancellationToken);
}
/// <summary>
/// Determines the number of tokens that match the specified query.
/// </summary>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query.
/// </returns>
public abstract Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken);
/// <summary>
/// Creates a new token.
/// </summary>
/// <param name="token">The token to create.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task CreateAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Removes a token.
/// </summary>
/// <param name="token">The token to delete.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
public abstract Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application.
/// </returns>
public virtual Task<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TToken> Query(IQueryable<TToken> 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);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
public virtual Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TToken> Query(IQueryable<TToken> 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);
}
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public virtual Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TToken> Query(IQueryable<TToken> tokens, TKey key)
=> from token in tokens
where token.Id.Equals(key)
select token;
return GetAsync((tokens, key) => Query(tokens, key), ConvertIdentifierFromString(identifier), cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public virtual Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier));
}
IQueryable<TToken> Query(IQueryable<TToken> tokens, string id)
=> from token in tokens
where token.ReferenceId == id
select token;
return GetAsync((tokens, id) => Query(tokens, identifier), identifier, cancellationToken);
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public virtual Task<ImmutableArray<TToken>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(subject))
{
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
}
IQueryable<TToken> Query(IQueryable<TToken> tokens, string principal)
=> from token in tokens
where token.Subject == principal
select token;
return ListAsync((tokens, principal) => Query(tokens, principal), subject, cancellationToken);
}
/// <summary>
/// Executes the specified query and returns the first element.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the first element returned when executing the query.
/// </returns>
public abstract Task<TResult> GetAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the optional application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the application identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetApplicationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.Application != null)
{
return new ValueTask<string>(ConvertIdentifierToString(token.Application.Id));
}
async Task<string> RetrieveApplicationIdAsync()
{
IQueryable<TKey> Query(IQueryable<TToken> 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<string>(RetrieveApplicationIdAsync());
}
/// <summary>
/// Retrieves the optional authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetAuthorizationIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (token.Authorization != null)
{
return new ValueTask<string>(ConvertIdentifierToString(token.Authorization.Id));
}
async Task<string> RetrieveAuthorizationIdAsync()
{
IQueryable<TKey> Query(IQueryable<TToken> 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<string>(RetrieveAuthorizationIdAsync());
}
/// <summary>
/// Retrieves the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the creation date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetCreationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.CreationDate);
}
/// <summary>
/// Retrieves the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the expiration date associated with the specified token.
/// </returns>
public virtual ValueTask<DateTimeOffset?> GetExpirationDateAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<DateTimeOffset?>(token.ExpirationDate);
}
/// <summary>
/// Retrieves the unique identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the unique identifier associated with the token.
/// </returns>
public virtual ValueTask<string> GetIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(ConvertIdentifierToString(token.Id));
}
/// <summary>
/// Retrieves the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the payload associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetPayloadAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Payload);
}
/// <summary>
/// Retrieves the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the additional properties associated with the token.
/// </returns>
public virtual ValueTask<JObject> GetPropertiesAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
if (string.IsNullOrEmpty(token.Properties))
{
return new ValueTask<JObject>(new JObject());
}
return new ValueTask<JObject>(JObject.Parse(token.Properties));
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the reference identifier associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetReferenceIdAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.ReferenceId);
}
/// <summary>
/// Retrieves the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Status);
}
/// <summary>
/// Retrieves the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Subject);
}
/// <summary>
/// Retrieves the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token.
/// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
return new ValueTask<string>(token.Type);
}
/// <summary>
/// Instantiates a new token.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the instantiated token, that can be persisted in the database.
/// </returns>
public virtual ValueTask<TToken> InstantiateAsync(CancellationToken cancellationToken)
=> new ValueTask<TToken>(new TToken());
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <param name="count">The number of results to return.</param>
/// <param name="offset">The number of results to skip.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public virtual Task<ImmutableArray<TToken>> ListAsync([CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken)
{
IQueryable<TToken> Query(IQueryable<TToken> 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);
}
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
/// </summary>
/// <typeparam name="TState">The state type.</typeparam>
/// <typeparam name="TResult">The result type.</typeparam>
/// <param name="query">The query to execute.</param>
/// <param name="state">The optional state.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the elements returned when executing the specified query.
/// </returns>
public abstract Task<ImmutableArray<TResult>> ListAsync<TState, TResult>(
[NotNull] Func<IQueryable<TToken>, TState, IQueryable<TResult>> query,
[CanBeNull] TState state, CancellationToken cancellationToken);
/// <summary>
/// Removes the tokens that are marked as expired or invalid.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task PruneAsync(CancellationToken cancellationToken);
/// <summary>
/// Sets the authorization identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task SetAuthorizationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the application identifier associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task SetApplicationIdAsync([NotNull] TToken token, [CanBeNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Sets the creation date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The creation date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the expiration date associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="date">The expiration date.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the payload associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="payload">The payload associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the additional properties associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="properties">The additional properties associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="identifier">The reference identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the status associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The status associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the subject associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Sets the token type associated with a token.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="type">The token type associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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;
}
/// <summary>
/// Updates an existing token.
/// </summary>
/// <param name="token">The token to update.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public abstract Task UpdateAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Converts the provided identifier to a strongly typed key object.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>An instance of <typeparamref name="TKey"/> representing the provided identifier.</returns>
public virtual TKey ConvertIdentifierFromString([CanBeNull] string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
return default;
}
return (TKey) TypeDescriptor.GetConverter(typeof(TKey)).ConvertFromInvariantString(identifier);
}
/// <summary>
/// Converts the provided identifier to its string representation.
/// </summary>
/// <param name="identifier">The identifier to convert.</param>
/// <returns>A <see cref="string"/> representation of the provided identifier.</returns>
public virtual string ConvertIdentifierToString([CanBeNull] TKey identifier)
{
if (Equals(identifier, default(TKey)))
{
return null;
}
return TypeDescriptor.GetConverter(typeof(TKey)).ConvertToInvariantString(identifier);
}
}
}

5
src/OpenIddict/OpenIddict.csproj

@ -14,13 +14,8 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" /> <ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.Models\OpenIddict.Models.csproj" />
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" /> <ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" /> <ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
</ItemGroup>
</Project> </Project>

57
src/OpenIddict/OpenIddictExtensions.cs

@ -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
{
/// <summary>
/// Configures OpenIddict to use the default entities, with the default entity key type (string).
/// The default entities are <see cref="OpenIddictApplication"/>, <see cref="OpenIddictAuthorization"/>,
/// <see cref="OpenIddictScope"/> and <see cref="OpenIddictToken"/>.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder UseDefaultModels([NotNull] this OpenIddictCoreBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.UseCustomModels<OpenIddictApplication,
OpenIddictAuthorization,
OpenIddictScope,
OpenIddictToken>();
}
/// <summary>
/// Configures OpenIddict to use the default entities, with the specified entity key type.
/// The default entities are <see cref="OpenIddictApplication{TKey}"/>, <see cref="OpenIddictAuthorization{TKey}"/>,
/// <see cref="OpenIddictScope{TKey}"/> and <see cref="OpenIddictToken{TKey}"/>.
/// </summary>
/// <typeparam name="TKey">The type of the entity primary keys.</typeparam>
/// <param name="builder">The services builder used by OpenIddict to register new services</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public static OpenIddictCoreBuilder UseDefaultModels<TKey>([NotNull] this OpenIddictCoreBuilder builder)
where TKey : IEquatable<TKey>
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return builder.UseCustomModels<OpenIddictApplication<TKey>,
OpenIddictAuthorization<TKey>,
OpenIddictScope<TKey>,
OpenIddictToken<TKey>>();
}
}
}

10
test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs

@ -356,15 +356,19 @@ namespace OpenIddict.Core.Tests
} }
[Fact] [Fact]
public void UseCustomModels_CustomEntitiesAreCorrectlySet() public void RegisterDefaultModels_CustomEntitiesAreCorrectlySet()
{ {
// Arrange // Arrange
var services = CreateServices(); var services = CreateServices();
var builder = CreateBuilder(services); var builder = CreateBuilder(services);
// Act // Act
services.AddOpenIddict().AddCore() services.AddOpenIddict()
.UseCustomModels<CustomApplication, CustomAuthorization, CustomScope, CustomToken>(); .AddCore()
.SetDefaultApplicationEntity<CustomApplication>()
.SetDefaultAuthorizationEntity<CustomAuthorization>()
.SetDefaultScopeEntity<CustomScope>()
.SetDefaultTokenEntity<CustomToken>();
// Assert // Assert
var provider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();

57
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<IOptionsMonitor<OpenIddictCoreOptions>>().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);
}
}
}

35
test/OpenIddict.EntityFramework.Tests/OpenIddictExtensionsTests.cs

@ -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<DbContext>))]
[InlineData(typeof(OpenIddictAuthorizationStoreResolver<DbContext>))]
[InlineData(typeof(OpenIddictScopeStoreResolver<DbContext>))]
[InlineData(typeof(OpenIddictTokenStoreResolver<DbContext>))]
public void AddEntityFrameworkStores_RegistersEntityFrameworkStoreFactories(Type type)
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddOpenIddict().AddCore();
// Act
builder.AddEntityFrameworkStores<DbContext>();
// Assert
Assert.Contains(services, service => service.ImplementationType == type);
}
}
}

32
test/OpenIddict.Tests/OpenIddictExtensionsTests.cs → test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictEntityFrameworkCoreExtensionsTests.cs

@ -8,22 +8,40 @@ using System;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models; using OpenIddict.EntityFrameworkCore.Models;
using Xunit; 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] [Fact]
public void UseDefaultModels_KeyTypeDefaultsToString() public void UseEntityFrameworkCore_KeyTypeDefaultsToString()
{ {
// Arrange // Arrange
var services = new ServiceCollection(); var services = new ServiceCollection();
var builder = services.AddOpenIddict().AddCore(); var builder = services.AddOpenIddict().AddCore();
// Act // Act
builder.UseDefaultModels(); builder.UseEntityFrameworkCore();
// Assert // Assert
var provider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();
@ -36,14 +54,14 @@ namespace OpenIddict.Tests
} }
[Fact] [Fact]
public void UseDefaultModels_KeyTypeCanBeOverriden() public void UseEntityFrameworkCore_KeyTypeCanBeOverriden()
{ {
// Arrange // Arrange
var services = new ServiceCollection(); var services = new ServiceCollection();
var builder = services.AddOpenIddict().AddCore(); var builder = services.AddOpenIddict().AddCore();
// Act // Act
builder.UseDefaultModels<Guid>(); builder.UseEntityFrameworkCore().ReplaceDefaultEntities<Guid>();
// Assert // Assert
var provider = services.BuildServiceProvider(); var provider = services.BuildServiceProvider();

34
test/OpenIddict.EntityFrameworkCore.Tests/OpenIddictExtensionsTests.cs

@ -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<DbContext>))]
[InlineData(typeof(OpenIddictAuthorizationStoreResolver<DbContext>))]
[InlineData(typeof(OpenIddictScopeStoreResolver<DbContext>))]
[InlineData(typeof(OpenIddictTokenStoreResolver<DbContext>))]
public void AddEntityFrameworkCoreStores_RegistersEntityFrameworkCoreStoreFactories(Type type)
{
// Arrange
var services = new ServiceCollection();
var builder = services.AddOpenIddict().AddCore();
// Act
builder.AddEntityFrameworkCoreStores<DbContext>();
// Assert
Assert.Contains(services, service => service.ImplementationType == type);
}
}
}

14
test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs

@ -317,7 +317,14 @@ namespace OpenIddict.Server.Tests
services.AddDistributedMemoryCache(); services.AddDistributedMemoryCache();
services.AddOpenIddict() services.AddOpenIddict()
.AddCore(options => options.UseDefaultModels()) .AddCore(options =>
{
options.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
})
.AddServer(options => configuration?.Invoke(options)); .AddServer(options => configuration?.Invoke(options));
}); });
@ -330,5 +337,10 @@ namespace OpenIddict.Server.Tests
return new TestServer(builder); return new TestServer(builder);
} }
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
} }
} }

1
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Authentication.cs

@ -16,7 +16,6 @@ using Moq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Bson; using Newtonsoft.Json.Bson;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests

1
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests

1
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs

@ -14,7 +14,6 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests

1
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs

@ -17,7 +17,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Moq; using Moq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests

76
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs

@ -16,7 +16,6 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests
@ -1584,11 +1583,7 @@ namespace OpenIddict.Server.Tests
public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted() public async Task SerializeAccessToken_ReferenceAccessTokenIsCorrectlyPersisted()
{ {
// Arrange // Arrange
var token = new OpenIddictToken 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 manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
@ -1610,8 +1605,9 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => builder.Configure(options =>
{ {
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.CreationDate.Value); options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero));
options.AccessTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; 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( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == token.ExpirationDate && descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.CreationDate == token.CreationDate && descriptor.CreationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload != null && descriptor.Payload != null &&
descriptor.ReferenceId != null && descriptor.ReferenceId != null &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&
@ -1821,11 +1817,7 @@ namespace OpenIddict.Server.Tests
public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted() public async Task SerializeAuthorizationCode_AuthorizationCodeIsCorrectlyPersisted()
{ {
// Arrange // Arrange
var token = new OpenIddictToken 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 manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
@ -1867,8 +1859,9 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => builder.Configure(options =>
{ {
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.CreationDate.Value); options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero));
options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; 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( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == token.ExpirationDate && descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) &&
descriptor.CreationDate == token.CreationDate && descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload == null && descriptor.Payload == null &&
descriptor.ReferenceId == null && descriptor.ReferenceId == null &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&
@ -1900,11 +1893,7 @@ namespace OpenIddict.Server.Tests
public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted() public async Task SerializeAuthorizationCode_ReferenceAuthorizationCodeIsCorrectlyPersisted()
{ {
// Arrange // Arrange
var token = new OpenIddictToken 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 manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
@ -1951,8 +1940,9 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => builder.Configure(options =>
{ {
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.CreationDate.Value); options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero));
options.AuthorizationCodeLifetime = token.ExpirationDate.Value - token.CreationDate.Value; 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( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == token.ExpirationDate && descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) &&
descriptor.CreationDate == token.CreationDate && descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload != null && descriptor.Payload != null &&
descriptor.ReferenceId != null && descriptor.ReferenceId != null &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&
@ -2232,11 +2222,7 @@ namespace OpenIddict.Server.Tests
public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted() public async Task SerializeRefreshToken_RefreshTokenIsCorrectlyPersisted()
{ {
// Arrange // Arrange
var token = new OpenIddictToken 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 manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
@ -2253,8 +2239,9 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => builder.Configure(options =>
{ {
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.CreationDate.Value); options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero));
options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; 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( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == token.ExpirationDate && descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) &&
descriptor.CreationDate == token.CreationDate && descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload == null && descriptor.Payload == null &&
descriptor.ReferenceId == null && descriptor.ReferenceId == null &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&
@ -2287,11 +2274,7 @@ namespace OpenIddict.Server.Tests
public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted() public async Task SerializeRefreshToken_ReferenceRefreshTokenIsCorrectlyPersisted()
{ {
// Arrange // Arrange
var token = new OpenIddictToken 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 manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
@ -2313,8 +2296,9 @@ namespace OpenIddict.Server.Tests
builder.Configure(options => builder.Configure(options =>
{ {
options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == token.CreationDate.Value); options.SystemClock = Mock.Of<ISystemClock>(mock => mock.UtcNow == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero));
options.RefreshTokenLifetime = token.ExpirationDate.Value - token.CreationDate.Value; 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( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == token.ExpirationDate && descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) &&
descriptor.CreationDate == token.CreationDate && descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload != null && descriptor.Payload != null &&
descriptor.ReferenceId == "B1F0D503-55A4-4B03-B05B-EF07713C18E1" && descriptor.ReferenceId == "B1F0D503-55A4-4B03-B05B-EF07713C18E1" &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&

11
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs

@ -30,7 +30,6 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Server.Tests namespace OpenIddict.Server.Tests
@ -1347,7 +1346,10 @@ namespace OpenIddict.Server.Tests
services.AddOpenIddict() services.AddOpenIddict()
.AddCore(options => .AddCore(options =>
{ {
options.UseDefaultModels(); options.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
options.Services.AddSingleton(CreateApplicationManager()); options.Services.AddSingleton(CreateApplicationManager());
options.Services.AddSingleton(CreateAuthorizationManager()); options.Services.AddSingleton(CreateAuthorizationManager());
@ -1546,5 +1548,10 @@ namespace OpenIddict.Server.Tests
return manager.Object; return manager.Object;
} }
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
} }
} }

14
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -671,7 +671,14 @@ namespace OpenIddict.Server.Tests
private static OpenIddictServerBuilder CreateBuilder(IServiceCollection services) private static OpenIddictServerBuilder CreateBuilder(IServiceCollection services)
=> services.AddOpenIddict() => services.AddOpenIddict()
.AddCore(options => options.UseDefaultModels()) .AddCore(options =>
{
options.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
})
.AddServer(); .AddServer();
private static IServiceCollection CreateServices() private static IServiceCollection CreateServices()
@ -695,5 +702,10 @@ namespace OpenIddict.Server.Tests
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>(); var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>();
return options.Get(OpenIddictServerDefaults.AuthenticationScheme); return options.Get(OpenIddictServerDefaults.AuthenticationScheme);
} }
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
} }
} }

27
test/OpenIddict.Tests/OpenIddict.Tests.csproj

@ -1,27 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\build\tests.props" />
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFrameworks Condition=" $([Microsoft.Build.Utilities.ToolLocationHelper]::GetPathToReferenceAssemblies('.NETFramework', '4.7', '').Count) != 0 ">$(TargetFrameworks);net47</TargetFrameworks>
<TargetFrameworks Condition=" '$(OS)' != 'Windows_NT' ">netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict\OpenIddict.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="xunit" Version="$(XunitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitVersion)" />
</ItemGroup>
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
</Project>

11
test/OpenIddict.Validation.Tests/OpenIddictValidationHandlerTests.cs

@ -27,7 +27,6 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Core; using OpenIddict.Core;
using OpenIddict.Models;
using Xunit; using Xunit;
namespace OpenIddict.Validation.Tests namespace OpenIddict.Validation.Tests
@ -687,6 +686,11 @@ namespace OpenIddict.Validation.Tests
services.AddOpenIddict() services.AddOpenIddict()
.AddCore(options => .AddCore(options =>
{ {
options.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>()
.SetDefaultTokenEntity<OpenIddictToken>();
// Replace the default OpenIddict managers. // Replace the default OpenIddict managers.
options.Services.AddSingleton(CreateApplicationManager()); options.Services.AddSingleton(CreateApplicationManager());
options.Services.AddSingleton(CreateAuthorizationManager()); options.Services.AddSingleton(CreateAuthorizationManager());
@ -824,5 +828,10 @@ namespace OpenIddict.Validation.Tests
return manager.Object; return manager.Object;
} }
public class OpenIddictApplication { }
public class OpenIddictAuthorization { }
public class OpenIddictScope { }
public class OpenIddictToken { }
} }
} }

Loading…
Cancel
Save