diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index 20864380..f6e4981b 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -357,7 +357,7 @@ namespace OpenIddict.Abstractions Task PopulateAsync([NotNull] object authorization, [NotNull] OpenIddictAuthorizationDescriptor descriptor, CancellationToken cancellationToken = default); /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs index 7e6ee472..3cd9825b 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs @@ -279,7 +279,7 @@ namespace OpenIddict.Abstractions [CanBeNull] TState state, CancellationToken cancellationToken); /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index da7312db..3644a105 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -887,7 +887,7 @@ namespace OpenIddict.Core } /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs index 73e7db65..b0478dda 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -691,7 +691,7 @@ namespace OpenIddict.EntityFramework } /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// @@ -735,7 +735,8 @@ namespace OpenIddict.EntityFramework 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)) + !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && + token.ExpirationDate > DateTimeOffset.UtcNow)) orderby authorization.Id select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs index 33db32b3..ba2f82bd 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -810,7 +810,9 @@ namespace OpenIddict.EntityFramework } } - for (var offset = 0; offset < 100_000; offset = offset + 1_000) + // Note: to avoid sending too many queries, the maximum number of elements + // that can be removed by a single call to PruneAsync() is limited to 50000. + for (var offset = 0; offset < 50_000; offset = offset + 1_000) { cancellationToken.ThrowIfCancellationRequested(); @@ -822,8 +824,8 @@ namespace OpenIddict.EntityFramework { var tokens = await (from token in Tokens - where token.ExpirationDate < DateTimeOffset.UtcNow || - token.Status != OpenIddictConstants.Statuses.Valid + where token.Status != OpenIddictConstants.Statuses.Valid || + token.ExpirationDate < DateTimeOffset.UtcNow orderby token.Id select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index fda4772b..9e5cab67 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -780,7 +780,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// @@ -834,7 +834,8 @@ namespace OpenIddict.EntityFrameworkCore 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)) + !authorization.Tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && + token.ExpirationDate > DateTimeOffset.UtcNow)) orderby authorization.Id select authorization).Skip(offset).Take(1_000).ToListAsync(cancellationToken); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index dbe401a0..a58dad06 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -935,7 +935,9 @@ namespace OpenIddict.EntityFrameworkCore return null; } - for (var offset = 0; offset < 100_000; offset = offset + 1_000) + // Note: to avoid sending too many queries, the maximum number of elements + // that can be removed by a single call to PruneAsync() is limited to 50000. + for (var offset = 0; offset < 50_000; offset = offset + 1_000) { cancellationToken.ThrowIfCancellationRequested(); @@ -947,8 +949,8 @@ namespace OpenIddict.EntityFrameworkCore { var tokens = await (from token in Tokens.AsTracking() - where token.ExpirationDate < DateTimeOffset.UtcNow || - token.Status != OpenIddictConstants.Statuses.Valid + where token.Status != OpenIddictConstants.Statuses.Valid || + token.ExpirationDate < DateTimeOffset.UtcNow orderby token.Id select token).Skip(offset).Take(1_000).ToListAsync(cancellationToken); diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs index 6292807d..1b9277ca 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -615,7 +615,7 @@ namespace OpenIddict.MongoDb } /// - /// Removes the ad-hoc authorizations that are marked as invalid or have no valid token attached. + /// Removes the ad-hoc authorizations that are marked as invalid or have no valid/nonexpired token attached. /// /// The that can be used to abort the operation. /// @@ -626,17 +626,57 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + // Note: directly deleting the resulting set of an aggregate query is not supported by MongoDB. + // To work around this limitation, the authorization identifiers are stored in an intermediate + // list and delete requests are sent to remove the documents corresponding to these identifiers. + var identifiers = await (from authorization in collection.AsQueryable() join token in database.GetCollection(Options.CurrentValue.TokensCollectionName).AsQueryable() on authorization.Id equals token.AuthorizationId into tokens where authorization.Status != OpenIddictConstants.Statuses.Valid || (authorization.Type == OpenIddictConstants.AuthorizationTypes.AdHoc && - !tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid)) - orderby authorization.Id + !tokens.Any(token => token.Status == OpenIddictConstants.Statuses.Valid && + token.ExpirationDate > DateTime.UtcNow)) select authorization.Id).ToListAsync(cancellationToken); - await collection.DeleteManyAsync(authorization => identifiers.Contains(authorization.Id)); + // Note: to avoid generating delete requests with very large filters, a buffer is used here and the + // maximum number of elements that can be removed by a single call to PruneAsync() is limited to 50000. + foreach (var buffer in Buffer(identifiers.Take(50_000), 1_000)) + { + await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id)); + + // Delete the tokens associated with the pruned authorizations. + await database.GetCollection(Options.CurrentValue.TokensCollectionName) + .DeleteManyAsync(token => buffer.Contains(token.AuthorizationId), cancellationToken); + } + + IEnumerable> Buffer(IEnumerable source, int count) + { + List buffer = null; + + foreach (var element in source) + { + if (buffer == null) + { + buffer = new List(); + } + + buffer.Add(element); + + if (buffer.Count == count) + { + yield return buffer; + + buffer = null; + } + } + + if (buffer != null) + { + yield return buffer; + } + } } /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs index 0cd99cea..f602c80c 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -712,8 +712,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); - await collection.DeleteManyAsync(token => token.ExpirationDate < DateTimeOffset.UtcNow || - token.Status != OpenIddictConstants.Statuses.Valid, cancellationToken); + await collection.DeleteManyAsync(token => token.Status != OpenIddictConstants.Statuses.Valid || + token.ExpirationDate < DateTime.UtcNow, cancellationToken); } ///