From f8bed3b7eec4cd6266a4026396ba876f5e8e0a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 17 Oct 2018 14:49:54 +0200 Subject: [PATCH 1/5] Remove an unnecessary private field --- src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs | 2 -- src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs | 2 -- src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs | 2 -- src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs | 2 -- 4 files changed, 8 deletions(-) diff --git a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs index 8b11df1e..8a21ac37 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs @@ -23,7 +23,6 @@ namespace OpenIddict.Core { private readonly IMemoryCache _cache; private readonly IOpenIddictApplicationStore _store; - private readonly IOptionsMonitor _options; public OpenIddictApplicationCache( [NotNull] IOptionsMonitor options, @@ -34,7 +33,6 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); - _options = options; _store = resolver.Get(); } diff --git a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs index 8e51c15e..f453e50a 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs @@ -23,7 +23,6 @@ namespace OpenIddict.Core { private readonly IMemoryCache _cache; private readonly IOpenIddictAuthorizationStore _store; - private readonly IOptionsMonitor _options; public OpenIddictAuthorizationCache( [NotNull] IOptionsMonitor options, @@ -34,7 +33,6 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); - _options = options; _store = resolver.Get(); } diff --git a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs index 4bac92a8..bec4c35e 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs @@ -24,7 +24,6 @@ namespace OpenIddict.Core { private readonly IMemoryCache _cache; private readonly IOpenIddictScopeStore _store; - private readonly IOptionsMonitor _options; public OpenIddictScopeCache( [NotNull] IOptionsMonitor options, @@ -35,7 +34,6 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); - _options = options; _store = resolver.Get(); } diff --git a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs index 37873e1a..121fb5b3 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs @@ -23,7 +23,6 @@ namespace OpenIddict.Core { private readonly IMemoryCache _cache; private readonly IOpenIddictTokenStore _store; - private readonly IOptionsMonitor _options; public OpenIddictTokenCache( [NotNull] IOptionsMonitor options, @@ -34,7 +33,6 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); - _options = options; _store = resolver.Get(); } From 3a13c8505a87b2b027b8ff1141a9846529b74b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Thu, 18 Oct 2018 17:58:01 +0200 Subject: [PATCH 2/5] Update the EF 6.x/EF Core entity configurations to use pre-defined column lengths --- .../OpenIddictApplicationConfiguration.cs | 9 ++++-- .../OpenIddictAuthorizationConfiguration.cs | 4 +++ .../OpenIddictScopeConfiguration.cs | 8 +++-- .../OpenIddictTokenConfiguration.cs | 12 ++++++- .../OpenIddictApplicationConfiguration.cs | 19 +++++++++-- .../OpenIddictAuthorizationConfiguration.cs | 23 +++++++++++-- .../OpenIddictScopeConfiguration.cs | 14 +++++++- .../OpenIddictTokenConfiguration.cs | 32 +++++++++++++++++-- 8 files changed, 107 insertions(+), 14 deletions(-) diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs index 3cb81bf3..1d6749af 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictApplicationConfiguration.cs @@ -48,14 +48,19 @@ namespace OpenIddict.EntityFramework HasKey(application => application.Id); Property(application => application.ClientId) + .HasMaxLength(100) .IsRequired() - .HasMaxLength(450) - .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute())); + .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute + { + IsUnique = true + })); Property(application => application.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); Property(application => application.Type) + .HasMaxLength(25) .IsRequired(); HasMany(application => application.Authorizations) diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs index 6d30cc12..4cdc0625 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs @@ -46,15 +46,19 @@ namespace OpenIddict.EntityFramework HasKey(authorization => authorization.Id); Property(authorization => authorization.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); Property(authorization => authorization.Status) + .HasMaxLength(25) .IsRequired(); Property(authorization => authorization.Subject) + .HasMaxLength(450) .IsRequired(); Property(authorization => authorization.Type) + .HasMaxLength(25) .IsRequired(); HasMany(authorization => authorization.Tokens) diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs index 6748215f..bb917e34 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictScopeConfiguration.cs @@ -44,12 +44,16 @@ namespace OpenIddict.EntityFramework HasKey(scope => scope.Id); Property(scope => scope.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); Property(scope => scope.Name) + .HasMaxLength(200) .IsRequired() - .HasMaxLength(450) - .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute())); + .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute + { + IsUnique = true + })); ToTable("OpenIddictScopes"); } diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs index ef442392..330c815b 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs +++ b/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs @@ -48,16 +48,26 @@ namespace OpenIddict.EntityFramework HasKey(token => token.Id); Property(token => token.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); + // Warning: the index on the ReferenceId property MUST NOT be declared as + // a unique index, as Entity Framework 6.x doesn't support creating indexes + // with null-friendly WHERE conditions, unlike Entity Framework Core 1.x/2.x. Property(token => token.ReferenceId) - .HasMaxLength(450) + .HasMaxLength(100) .HasColumnAnnotation(IndexAnnotation.AnnotationName, new IndexAnnotation(new IndexAttribute())); + Property(token => token.Status) + .HasMaxLength(25) + .IsRequired(); + Property(token => token.Subject) + .HasMaxLength(450) .IsRequired(); Property(token => token.Type) + .HasMaxLength(25) .IsRequired(); ToTable("OpenIddictTokens"); diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs index 5c96953c..b766df87 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictApplicationConfiguration.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using OpenIddict.EntityFrameworkCore.Models; @@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore where TToken : OpenIddictToken where TKey : IEquatable { - public void Configure(EntityTypeBuilder builder) + public void Configure([NotNull] EntityTypeBuilder builder) { if (builder == null) { @@ -37,28 +38,40 @@ namespace OpenIddict.EntityFrameworkCore // Entity Framework would throw an exception due to the TKey generic parameter // being non-nullable when using value types like short, int, long or Guid. + // If primary/foreign keys are strings, limit their length to ensure + // they can be safely used in indexes, specially when the underlying + // provider is known to not restrict the default length (e.g MySQL). + if (typeof(TKey) == typeof(string)) + { + builder.Property(application => application.Id) + .HasMaxLength(50); + } + builder.HasKey(application => application.Id); builder.HasIndex(application => application.ClientId) .IsUnique(); builder.Property(application => application.ClientId) + .HasMaxLength(100) .IsRequired(); builder.Property(application => application.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); builder.Property(application => application.Type) + .HasMaxLength(25) .IsRequired(); builder.HasMany(application => application.Authorizations) .WithOne(authorization => authorization.Application) - .HasForeignKey("ApplicationId") + .HasForeignKey(nameof(OpenIddictAuthorization.Application) + nameof(OpenIddictApplication.Id)) .IsRequired(required: false); builder.HasMany(application => application.Tokens) .WithOne(token => token.Application) - .HasForeignKey("ApplicationId") + .HasForeignKey(nameof(OpenIddictToken.Application) + nameof(OpenIddictApplication.Id)) .IsRequired(required: false); builder.ToTable("OpenIddictApplications"); diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs index 477d6b9a..a9906c63 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using OpenIddict.EntityFrameworkCore.Models; @@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore where TToken : OpenIddictToken where TKey : IEquatable { - public void Configure(EntityTypeBuilder builder) + public void Configure([NotNull] EntityTypeBuilder builder) { if (builder == null) { @@ -37,24 +38,40 @@ namespace OpenIddict.EntityFrameworkCore // Entity Framework would throw an exception due to the TKey generic parameter // being non-nullable when using value types like short, int, long or Guid. + // If primary/foreign keys are strings, limit their length to ensure + // they can be safely used in indexes, specially when the underlying + // provider is known to not restrict the default length (e.g MySQL). + if (typeof(TKey) == typeof(string)) + { + builder.Property(nameof(OpenIddictAuthorization.Application) + nameof(OpenIddictApplication.Id)) + .HasMaxLength(50); + + builder.Property(application => application.Id) + .HasMaxLength(50); + } + builder.HasKey(authorization => authorization.Id); - builder.HasIndex("ApplicationId", - nameof(OpenIddictAuthorization.Scopes), + builder.HasIndex( + nameof(OpenIddictAuthorization.Application) + nameof(OpenIddictApplication.Id), nameof(OpenIddictAuthorization.Status), nameof(OpenIddictAuthorization.Subject), nameof(OpenIddictAuthorization.Type)); builder.Property(authorization => authorization.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); builder.Property(authorization => authorization.Status) + .HasMaxLength(25) .IsRequired(); builder.Property(authorization => authorization.Subject) + .HasMaxLength(450) .IsRequired(); builder.Property(authorization => authorization.Type) + .HasMaxLength(25) .IsRequired(); builder.HasMany(authorization => authorization.Tokens) diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs index d76c220f..4acddc7b 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictScopeConfiguration.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using OpenIddict.EntityFrameworkCore.Models; @@ -22,7 +23,7 @@ namespace OpenIddict.EntityFrameworkCore where TScope : OpenIddictScope where TKey : IEquatable { - public void Configure(EntityTypeBuilder builder) + public void Configure([NotNull] EntityTypeBuilder builder) { if (builder == null) { @@ -33,15 +34,26 @@ namespace OpenIddict.EntityFrameworkCore // Entity Framework would throw an exception due to the TKey generic parameter // being non-nullable when using value types like short, int, long or Guid. + // If primary/foreign keys are strings, limit their length to ensure + // they can be safely used in indexes, specially when the underlying + // provider is known to not restrict the default length (e.g MySQL). + if (typeof(TKey) == typeof(string)) + { + builder.Property(scope => scope.Id) + .HasMaxLength(50); + } + builder.HasKey(scope => scope.Id); builder.HasIndex(scope => scope.Name) .IsUnique(); builder.Property(scope => scope.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); builder.Property(scope => scope.Name) + .HasMaxLength(200) .IsRequired(); builder.ToTable("OpenIddictScopes"); diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs index daa1dbee..3af9b181 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using OpenIddict.EntityFrameworkCore.Models; @@ -26,7 +27,7 @@ namespace OpenIddict.EntityFrameworkCore where TAuthorization : OpenIddictAuthorization where TKey : IEquatable { - public void Configure(EntityTypeBuilder builder) + public void Configure([NotNull] EntityTypeBuilder builder) { if (builder == null) { @@ -37,23 +38,50 @@ namespace OpenIddict.EntityFrameworkCore // Entity Framework would throw an exception due to the TKey generic parameter // being non-nullable when using value types like short, int, long or Guid. + + // If primary/foreign keys are strings, limit their length to ensure + // they can be safely used in indexes, specially when the underlying + // provider is known to not restrict the default length (e.g MySQL). + if (typeof(TKey) == typeof(string)) + { + builder.Property(nameof(OpenIddictToken.Application) + nameof(OpenIddictApplication.Id)) + .HasMaxLength(50); + + builder.Property(nameof(OpenIddictToken.Authorization) + nameof(OpenIddictApplication.Id)) + .HasMaxLength(50); + + builder.Property(token => token.Id) + .HasMaxLength(50); + } + builder.HasKey(token => token.Id); builder.HasIndex(token => token.ReferenceId) .IsUnique(); - builder.HasIndex("ApplicationId", + builder.HasIndex( + nameof(OpenIddictToken.Application) + nameof(OpenIddictApplication.Id), nameof(OpenIddictToken.Status), nameof(OpenIddictToken.Subject), nameof(OpenIddictToken.Type)); builder.Property(token => token.ConcurrencyToken) + .HasMaxLength(50) .IsConcurrencyToken(); + builder.Property(token => token.ReferenceId) + .HasMaxLength(100); + + builder.Property(token => token.Status) + .HasMaxLength(25) + .IsRequired(); + builder.Property(token => token.Subject) + .HasMaxLength(450) .IsRequired(); builder.Property(token => token.Type) + .HasMaxLength(25) .IsRequired(); builder.ToTable("OpenIddictTokens"); From d7de292d93bc86f8b0646b26d7feff468a5eb4ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 20 Oct 2018 14:28:31 +0200 Subject: [PATCH 3/5] Update OpenIddictMongoDbContext to dispose of the private semaphore --- .../OpenIddictMongoDbContext.cs | 12 ++++++++++- .../Stores/OpenIddictAuthorizationStore.cs | 2 ++ .../OpenIddictMongoDbContextTests.cs | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs index af3aaadf..9e987266 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs @@ -19,7 +19,7 @@ namespace OpenIddict.MongoDb /// /// Exposes the MongoDB database used by the OpenIddict stores. /// - public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext + public class OpenIddictMongoDbContext : IOpenIddictMongoDbContext, IDisposable { private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; @@ -35,6 +35,11 @@ namespace OpenIddict.MongoDb _semaphore = new SemaphoreSlim(1); } + /// + /// Disposes the semaphore held by this instance. + /// + public void Dispose() => _semaphore.Dispose(); + /// /// Gets the . /// @@ -49,6 +54,11 @@ namespace OpenIddict.MongoDb return new ValueTask(_database); } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + async Task ExecuteAsync() { var options = _options.CurrentValue; diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs index 9f7053c9..e805119e 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -299,6 +299,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + // Note: Enumerable.All() is deliberately used without the extension method syntax to ensure + // ImmutableArrayExtensions.All() (which is not supported by MongoDB) is not used instead. return ImmutableArray.CreateRange(await collection.Find(authorization => authorization.Subject == subject && authorization.ApplicationId == ObjectId.Parse(client) && diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs index 319cbc3e..830c67c0 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs @@ -20,6 +20,27 @@ namespace OpenIddict.MongoDb.Tests { public class OpenIddictMongoDbContextTests { + [Fact] + public async Task GetDatabaseAsync_ThrowsAnExceptionForCanceledToken() + { + // Arrange + var services = new ServiceCollection(); + var provider = services.BuildServiceProvider(); + + var options = Mock.Of>(); + var token = new CancellationToken(canceled: true); + + var context = new OpenIddictMongoDbContext(options, provider); + + // Act and assert + var exception = await Assert.ThrowsAsync(async delegate + { + await context.GetDatabaseAsync(token); + }); + + Assert.Equal(token, exception.CancellationToken); + } + [Fact] public async Task GetDatabaseAsync_ThrowsAnExceptionForNullOptions() { From 338f8779f4e5a4650452d98ae0922161f42ee314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 20 Oct 2018 19:13:55 +0200 Subject: [PATCH 4/5] Update the OpenIddict.Core extensions to register the memory cache services and remove unnecessary references --- src/OpenIddict.Core/OpenIddictCoreExtensions.cs | 1 + .../OpenIddict.EntityFramework.csproj | 1 - .../OpenIddictEntityFrameworkExtensions.cs | 2 -- .../OpenIddict.EntityFrameworkCore.csproj | 1 - .../OpenIddictEntityFrameworkCoreExtensions.cs | 2 -- src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj | 3 +-- src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs | 4 +--- 7 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs index f0c5ce72..72a75698 100644 --- a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs @@ -33,6 +33,7 @@ namespace Microsoft.Extensions.DependencyInjection } builder.Services.AddLogging(); + builder.Services.AddMemoryCache(); builder.Services.AddOptions(); builder.Services.TryAddScoped(typeof(OpenIddictApplicationManager<>)); diff --git a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj index fcfe5273..2b8eef02 100644 --- a/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj +++ b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.csproj @@ -19,7 +19,6 @@ - diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs index a1c3e3ea..41cf8066 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs @@ -32,8 +32,6 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddMemoryCache(); - // Since Entity Framework 6.x may be used with databases performing case-insensitive // or culture-sensitive comparisons, ensure the additional filtering logic is enforced // in case case-sensitive stores were registered before this extension was called. diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj index 1a45afbd..cdca6b5e 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj @@ -20,7 +20,6 @@ - diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs index 4198589e..7eccad4b 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -33,8 +33,6 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddMemoryCache(); - // Since Entity Framework Core may be used with databases performing case-insensitive // or culture-sensitive comparisons, ensure the additional filtering logic is enforced // in case case-sensitive stores were registered before this extension was called. diff --git a/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj index 9310c7e3..9cb05ac7 100644 --- a/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj +++ b/src/OpenIddict.MongoDb/OpenIddict.MongoDb.csproj @@ -1,4 +1,4 @@ - + @@ -21,7 +21,6 @@ - diff --git a/src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs b/src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs index feae9a22..6b03ad90 100644 --- a/src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs +++ b/src/OpenIddict.Mvc/Internal/OpenIddictMvcBinder.cs @@ -33,9 +33,7 @@ namespace OpenIddict.Mvc.Internal /// directly from your code. This API may change or be removed in future minor releases. /// public OpenIddictMvcBinder([NotNull] IOptionsMonitor options) - { - _options = options; - } + => _options = options; /// /// Tries to bind a model from the request. From 3a9b08b1090ea78fd8cd9fa4de43142b5f0c2404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 22 Oct 2018 13:35:50 +0200 Subject: [PATCH 5/5] Update the caches to use expiration tokens to remove invalid entries --- .../Caches/OpenIddictApplicationCache.cs | 166 ++++++++++-- .../Caches/OpenIddictAuthorizationCache.cs | 203 +++++++++++--- .../Caches/OpenIddictScopeCache.cs | 142 ++++++++-- .../Caches/OpenIddictTokenCache.cs | 251 ++++++++++++++---- .../Managers/OpenIddictApplicationManager.cs | 10 +- .../OpenIddictAuthorizationManager.cs | 10 +- .../Managers/OpenIddictScopeManager.cs | 10 +- .../Managers/OpenIddictTokenManager.cs | 10 +- 8 files changed, 663 insertions(+), 139 deletions(-) diff --git a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs index 8a21ac37..f9a75d8c 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs @@ -5,12 +5,14 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; namespace OpenIddict.Core @@ -21,7 +23,8 @@ namespace OpenIddict.Core /// The type of the Application entity. public class OpenIddictApplicationCache : IOpenIddictApplicationCache, IDisposable where TApplication : class { - private readonly IMemoryCache _cache; + private readonly MemoryCache _cache; + private readonly ConcurrentDictionary> _signals; private readonly IOpenIddictApplicationStore _store; public OpenIddictApplicationCache( @@ -33,6 +36,7 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); + _signals = new ConcurrentDictionary>(StringComparer.Ordinal); _store = resolver.Get(); } @@ -51,14 +55,51 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(application)); } + _cache.Remove(new + { + Method = nameof(FindByClientIdAsync), + Identifier = await _store.GetClientIdAsync(application, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(application, cancellationToken) + }); + + foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByPostLogoutRedirectUriAsync), + Address = address + }); + } + + foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByRedirectUriAsync), + Address = address + }); + } + + var signal = await CreateExpirationSignalAsync(application, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + using (var entry = _cache.CreateEntry(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(application, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(application); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(application); } using (var entry = _cache.CreateEntry(new @@ -67,15 +108,24 @@ namespace OpenIddict.Core Identifier = await _store.GetClientIdAsync(application, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(application); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(application); } } /// - /// Disposes the cache held by this instance. + /// Disposes the resources held by this instance. /// - public void Dispose() => _cache.Dispose(); + public void Dispose() + { + foreach (var signal in _signals) + { + signal.Value.Value.Dispose(); + } + + _cache.Dispose(); + } /// /// Retrieves an application using its client identifier. @@ -113,6 +163,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (application != null) + { + var signal = await CreateExpirationSignalAsync(application, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(application); } @@ -159,6 +220,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (application != null) + { + var signal = await CreateExpirationSignalAsync(application, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(application); } @@ -206,6 +278,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var application in applications) + { + var signal = await CreateExpirationSignalAsync(application, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(applications.Length); entry.SetValue(applications); } @@ -253,6 +336,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var application in applications) + { + var signal = await CreateExpirationSignalAsync(application, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(applications.Length); entry.SetValue(applications); } @@ -278,35 +372,53 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(application)); } - _cache.Remove(new + var identifier = await _store.GetIdAsync(application, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindByClientIdAsync), - Identifier = await _store.GetClientIdAsync(application, cancellationToken) - }); + throw new InvalidOperationException("The application identifier cannot be extracted."); + } - _cache.Remove(new + if (_signals.TryGetValue(identifier, out Lazy signal)) { - Method = nameof(FindByIdAsync), - Identifier = await _store.GetIdAsync(application, cancellationToken) - }); + signal.Value.Cancel(); - foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) + _signals.TryRemove(identifier, out signal); + } + } + + /// + /// Creates an expiration signal allowing to invalidate all the + /// cache entries associated with the specified application. + /// + /// The application associated with the expiration signal. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns an expiration signal for the specified application. + /// + protected virtual async Task CreateExpirationSignalAsync( + [NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) { - _cache.Remove(new - { - Method = nameof(FindByPostLogoutRedirectUriAsync), - Address = address - }); + throw new ArgumentNullException(nameof(application)); } - foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken)) + var identifier = await _store.GetIdAsync(application, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - _cache.Remove(new - { - Method = nameof(FindByRedirectUriAsync), - Address = address - }); + throw new InvalidOperationException("The application identifier cannot be extracted."); } + + var signal = _signals.GetOrAdd(identifier, delegate + { + // Note: a Lazy is used here to ensure only one CancellationTokenSource + // can be created. Not doing so would result in expiration signals being potentially linked to + // multiple sources, with a single one of them being eventually tracked and thus, cancelable. + return new Lazy(() => new CancellationTokenSource()); + }); + + return new CancellationChangeToken(signal.Value.Token); } } } diff --git a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs index f453e50a..53b673cd 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs @@ -5,12 +5,14 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; namespace OpenIddict.Core @@ -21,7 +23,8 @@ namespace OpenIddict.Core /// The type of the Authorization entity. public class OpenIddictAuthorizationCache : IOpenIddictAuthorizationCache, IDisposable where TAuthorization : class { - private readonly IMemoryCache _cache; + private readonly MemoryCache _cache; + private readonly ConcurrentDictionary> _signals; private readonly IOpenIddictAuthorizationStore _store; public OpenIddictAuthorizationCache( @@ -33,6 +36,7 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); + _signals = new ConcurrentDictionary>(StringComparer.Ordinal); _store = resolver.Get(); } @@ -51,21 +55,78 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(authorization)); } + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), + Status = await _store.GetStatusAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), + Status = await _store.GetStatusAsync(authorization, cancellationToken), + Type = await _store.GetTypeAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindBySubjectAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken) + }); + + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + using (var entry = _cache.CreateEntry(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(authorization, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(authorization); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(authorization); } } /// - /// Disposes the cache held by this instance. + /// Disposes the resources held by this instance. /// - public void Dispose() => _cache.Dispose(); + public void Dispose() + { + foreach (var signal in _signals) + { + signal.Value.Value.Dispose(); + } + + _cache.Dispose(); + } /// /// Retrieves the authorizations corresponding to the specified @@ -112,6 +173,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var authorization in authorizations) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } @@ -174,6 +246,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var authorization in authorizations) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } @@ -243,6 +326,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var authorization in authorizations) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } @@ -345,6 +439,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var authorization in authorizations) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } @@ -391,6 +496,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (authorization != null) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(authorization); } @@ -438,6 +554,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var authorization in authorizations) + { + var signal = await CreateExpirationTokenAsync(authorization, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(authorizations.Length); entry.SetValue(authorizations); } @@ -463,47 +590,53 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(authorization)); } - _cache.Remove(new + var identifier = await _store.GetIdAsync(authorization, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(authorization, cancellationToken), - Client = await _store.GetApplicationIdAsync(authorization, cancellationToken) - }); + throw new InvalidOperationException("The application identifier cannot be extracted."); + } - _cache.Remove(new + if (_signals.TryGetValue(identifier, out Lazy signal)) { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(authorization, cancellationToken), - Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), - Status = await _store.GetStatusAsync(authorization, cancellationToken) - }); + signal.Value.Cancel(); - _cache.Remove(new - { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(authorization, cancellationToken), - Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), - Status = await _store.GetStatusAsync(authorization, cancellationToken), - Type = await _store.GetTypeAsync(authorization, cancellationToken) - }); + _signals.TryRemove(identifier, out signal); + } + } - _cache.Remove(new + /// + /// Creates an expiration signal allowing to invalidate all the + /// cache entries associated with the specified authorization. + /// + /// The authorization associated with the expiration signal. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns an expiration signal for the specified authorization. + /// + protected virtual async Task CreateExpirationTokenAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) { - Method = nameof(FindByApplicationIdAsync), - Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken) - }); + throw new ArgumentNullException(nameof(authorization)); + } - _cache.Remove(new + var identifier = await _store.GetIdAsync(authorization, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindByIdAsync), - Identifier = await _store.GetIdAsync(authorization, cancellationToken) - }); + throw new InvalidOperationException("The authorization identifier cannot be extracted."); + } - _cache.Remove(new + var signal = _signals.GetOrAdd(identifier, delegate { - Method = nameof(FindBySubjectAsync), - Subject = await _store.GetSubjectAsync(authorization, cancellationToken) + // Note: a Lazy is used here to ensure only one CancellationTokenSource + // can be created. Not doing so would result in expiration signals being potentially linked to + // multiple sources, with a single one of them being eventually tracked and thus, cancelable. + return new Lazy(() => new CancellationTokenSource()); }); + + return new CancellationChangeToken(signal.Value.Token); } } } diff --git a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs index bec4c35e..f30dfdfb 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Linq; using System.Threading; @@ -12,6 +13,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; namespace OpenIddict.Core @@ -22,7 +24,8 @@ namespace OpenIddict.Core /// The type of the Scope entity. public class OpenIddictScopeCache : IOpenIddictScopeCache, IDisposable where TScope : class { - private readonly IMemoryCache _cache; + private readonly MemoryCache _cache; + private readonly ConcurrentDictionary> _signals; private readonly IOpenIddictScopeStore _store; public OpenIddictScopeCache( @@ -34,6 +37,7 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); + _signals = new ConcurrentDictionary>(StringComparer.Ordinal); _store = resolver.Get(); } @@ -52,14 +56,42 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(scope)); } + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(scope, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByNameAsync), + Name = await _store.GetNameAsync(scope, cancellationToken) + }); + + foreach (var resource in await _store.GetResourcesAsync(scope, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByResourceAsync), + Resource = resource + }); + } + + var signal = await CreateExpirationSignalAsync(scope, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration token."); + } + using (var entry = _cache.CreateEntry(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(scope, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(scope); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(scope); } using (var entry = _cache.CreateEntry(new @@ -68,15 +100,24 @@ namespace OpenIddict.Core Name = await _store.GetNameAsync(scope, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(scope); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(scope); } } /// - /// Disposes the cache held by this instance. + /// Disposes the resources held by this instance. /// - public void Dispose() => _cache.Dispose(); + public void Dispose() + { + foreach (var signal in _signals) + { + signal.Value.Value.Dispose(); + } + + _cache.Dispose(); + } /// /// Retrieves a scope using its unique identifier. @@ -114,6 +155,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (scope != null) + { + var signal = await CreateExpirationSignalAsync(scope, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(scope); } @@ -160,6 +212,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (scope != null) + { + var signal = await CreateExpirationSignalAsync(scope, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(scope); } @@ -244,6 +307,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var scope in scopes) + { + var signal = await CreateExpirationSignalAsync(scope, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(scopes.Length); entry.SetValue(scopes); } @@ -269,26 +343,52 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(scope)); } - _cache.Remove(new + var identifier = await _store.GetIdAsync(scope, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindByIdAsync), - Identifier = await _store.GetIdAsync(scope, cancellationToken) - }); + throw new InvalidOperationException("The application identifier cannot be extracted."); + } - _cache.Remove(new + if (_signals.TryGetValue(identifier, out Lazy signal)) { - Method = nameof(FindByNameAsync), - Name = await _store.GetNameAsync(scope, cancellationToken) - }); + signal.Value.Cancel(); - foreach (var resource in await _store.GetResourcesAsync(scope, cancellationToken)) + _signals.TryRemove(identifier, out signal); + } + } + + /// + /// Creates an expiration signal allowing to invalidate all the + /// cache entries associated with the specified scope. + /// + /// The scope associated with the expiration signal. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns an expiration signal for the specified scope. + /// + protected virtual async Task CreateExpirationSignalAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) { - _cache.Remove(new - { - Method = nameof(FindByResourceAsync), - Resource = resource - }); + throw new ArgumentNullException(nameof(scope)); } + + var identifier = await _store.GetIdAsync(scope, cancellationToken); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The scope identifier cannot be extracted."); + } + + var signal = _signals.GetOrAdd(identifier, delegate + { + // Note: a Lazy is used here to ensure only one CancellationTokenSource + // can be created. Not doing so would result in expiration signals being potentially linked to + // multiple sources, with a single one of them being eventually tracked and thus, cancelable. + return new Lazy(() => new CancellationTokenSource()); + }); + + return new CancellationChangeToken(signal.Value.Token); } } } diff --git a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs index 121fb5b3..d2e8d451 100644 --- a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs +++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs @@ -5,12 +5,14 @@ */ using System; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using OpenIddict.Abstractions; namespace OpenIddict.Core @@ -21,7 +23,8 @@ namespace OpenIddict.Core /// The type of the Token entity. public class OpenIddictTokenCache : IOpenIddictTokenCache, IDisposable where TToken : class { - private readonly IMemoryCache _cache; + private readonly MemoryCache _cache; + private readonly ConcurrentDictionary> _signals; private readonly IOpenIddictTokenStore _store; public OpenIddictTokenCache( @@ -33,6 +36,7 @@ namespace OpenIddict.Core SizeLimit = options.CurrentValue.EntityCacheLimit }); + _signals = new ConcurrentDictionary>(StringComparer.Ordinal); _store = resolver.Get(); } @@ -51,14 +55,75 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(token)); } + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken), + Status = await _store.GetStatusAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken), + Status = await _store.GetStatusAsync(token, cancellationToken), + Type = await _store.GetTypeAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = await _store.GetApplicationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByAuthorizationIdAsync), + Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByReferenceIdAsync), + Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindBySubjectAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken) + }); + + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + using (var entry = _cache.CreateEntry(new { Method = nameof(FindByIdAsync), Identifier = await _store.GetIdAsync(token, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(token); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(token); } using (var entry = _cache.CreateEntry(new @@ -67,15 +132,24 @@ namespace OpenIddict.Core Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) })) { - entry.SetSize(1L); - entry.SetValue(token); + entry.AddExpirationToken(signal) + .SetSize(1L) + .SetValue(token); } } /// - /// Disposes the cache held by this instance. + /// Disposes the resources held by this instance. /// - public void Dispose() => _cache.Dispose(); + public void Dispose() + { + foreach (var signal in _signals) + { + signal.Value.Value.Dispose(); + } + + _cache.Dispose(); + } /// /// Retrieves the tokens corresponding to the specified @@ -122,6 +196,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -184,6 +269,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -253,6 +349,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -300,6 +407,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -347,6 +465,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -393,6 +522,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (token != null) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(token); } @@ -440,6 +580,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + if (token != null) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(1L); entry.SetValue(token); } @@ -486,6 +637,17 @@ namespace OpenIddict.Core using (var entry = _cache.CreateEntry(parameters)) { + foreach (var token in tokens) + { + var signal = await CreateExpirationSignalAsync(token, cancellationToken); + if (signal == null) + { + throw new InvalidOperationException("An error occurred while creating an expiration signal."); + } + + entry.AddExpirationToken(signal); + } + entry.SetSize(tokens.Length); entry.SetValue(tokens); } @@ -511,59 +673,52 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(token)); } - _cache.Remove(new - { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(token, cancellationToken), - Client = await _store.GetApplicationIdAsync(token, cancellationToken) - }); - - _cache.Remove(new + var identifier = await _store.GetIdAsync(token, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(token, cancellationToken), - Client = await _store.GetApplicationIdAsync(token, cancellationToken), - Status = await _store.GetStatusAsync(token, cancellationToken) - }); + throw new InvalidOperationException("The application identifier cannot be extracted."); + } - _cache.Remove(new + if (_signals.TryGetValue(identifier, out Lazy signal)) { - Method = nameof(FindAsync), - Subject = await _store.GetSubjectAsync(token, cancellationToken), - Client = await _store.GetApplicationIdAsync(token, cancellationToken), - Status = await _store.GetStatusAsync(token, cancellationToken), - Type = await _store.GetTypeAsync(token, cancellationToken) - }); + signal.Value.Cancel(); - _cache.Remove(new - { - Method = nameof(FindByApplicationIdAsync), - Identifier = await _store.GetApplicationIdAsync(token, cancellationToken) - }); + _signals.TryRemove(identifier, out signal); + } + } - _cache.Remove(new + /// + /// Creates an expiration signal allowing to invalidate all the + /// cache entries associated with the specified token. + /// + /// The token associated with the expiration signal. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns an expiration signal for the specified token. + /// + protected virtual async Task CreateExpirationSignalAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) { - Method = nameof(FindByAuthorizationIdAsync), - Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken) - }); + throw new ArgumentNullException(nameof(token)); + } - _cache.Remove(new + var identifier = await _store.GetIdAsync(token, cancellationToken); + if (string.IsNullOrEmpty(identifier)) { - Method = nameof(FindByIdAsync), - Identifier = await _store.GetIdAsync(token, cancellationToken) - }); + throw new InvalidOperationException("The token identifier cannot be extracted."); + } - _cache.Remove(new + var signal = _signals.GetOrAdd(identifier, delegate { - Method = nameof(FindByReferenceIdAsync), - Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) + // Note: a Lazy is used here to ensure only one CancellationTokenSource + // can be created. Not doing so would result in expiration signals being potentially linked to + // multiple sources, with a single one of them being eventually tracked and thus, cancelable. + return new Lazy(() => new CancellationTokenSource()); }); - _cache.Remove(new - { - Method = nameof(FindBySubjectAsync), - Subject = await _store.GetSubjectAsync(token, cancellationToken) - }); + return new CancellationChangeToken(signal.Value.Token); } } } diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 48dd73e9..9437635a 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -157,6 +157,11 @@ namespace OpenIddict.Core } await Store.CreateAsync(application, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(application, cancellationToken); + } } /// @@ -899,12 +904,13 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + await Store.UpdateAsync(application, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) { await Cache.RemoveAsync(application, cancellationToken); + await Cache.AddAsync(application, cancellationToken); } - - await Store.UpdateAsync(application, cancellationToken); } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index beca6591..2ec888ee 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -126,6 +126,11 @@ namespace OpenIddict.Core } await Store.CreateAsync(authorization, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(authorization, cancellationToken); + } } /// @@ -1115,12 +1120,13 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + await Store.UpdateAsync(authorization, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) { await Cache.RemoveAsync(authorization, cancellationToken); + await Cache.AddAsync(authorization, cancellationToken); } - - await Store.UpdateAsync(authorization, cancellationToken); } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index 4ddf0772..cdf115dd 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -120,6 +120,11 @@ namespace OpenIddict.Core } await Store.CreateAsync(scope, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(scope, cancellationToken); + } } /// @@ -679,12 +684,13 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + await Store.UpdateAsync(scope, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) { await Cache.RemoveAsync(scope, cancellationToken); + await Cache.AddAsync(scope, cancellationToken); } - - await Store.UpdateAsync(scope, cancellationToken); } /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 96669932..4d016b72 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -134,6 +134,11 @@ namespace OpenIddict.Core } await Store.CreateAsync(token, cancellationToken); + + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.AddAsync(token, cancellationToken); + } } /// @@ -1180,12 +1185,13 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + await Store.UpdateAsync(token, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) { await Cache.RemoveAsync(token, cancellationToken); + await Cache.AddAsync(token, cancellationToken); } - - await Store.UpdateAsync(token, cancellationToken); } ///