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 a4ae113b..eed081b8 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 945ebaad..95b55f3d 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -699,7 +699,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. /// @@ -743,7 +743,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 d3772fb3..1fc62c77 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -824,7 +824,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(); @@ -836,8 +838,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 cf5f01ce..243e0883 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -752,7 +752,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. /// @@ -806,7 +806,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 88f70e71..a261b375 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -865,7 +865,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(); @@ -877,8 +879,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 55b7dc75..dcd92357 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -617,7 +617,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. /// @@ -628,17 +628,57 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.Value.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.Value.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.Value.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 b0861a8e..53974c1d 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -714,8 +714,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.Value.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); } ///