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);
}
///