diff --git a/Directory.Build.targets b/Directory.Build.targets
index 1ff00b31..51c5ac51 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -107,6 +107,7 @@
$(DefineConstants);SUPPORTS_AUTHENTICATION_HANDLER_SELECTION_FALLBACK
+ $(DefineConstants);SUPPORTS_BULK_DBSET_OPERATIONS
internal static class OpenIddictHelpers
{
+ ///
+ /// Generates a sequence of non-overlapping adjacent buffers over the source sequence.
+ ///
+ /// The source sequence element type.
+ /// The source sequence.
+ /// The number of elements for allocated buffers.
+ /// A sequence of buffers containing source sequence elements.
+ public static IEnumerable> Buffer(this IEnumerable source, int count)
+ {
+ List? buffer = null;
+
+ foreach (var element in source)
+ {
+ buffer ??= [];
+ buffer.Add(element);
+
+ if (buffer.Count == count)
+ {
+ yield return buffer;
+
+ buffer = null;
+ }
+ }
+
+ if (buffer is not null)
+ {
+ yield return buffer;
+ }
+ }
+
///
/// Finds the first base type that matches the specified generic type definition.
///
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
index a51e983f..23bacf25 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
@@ -391,10 +391,8 @@ public interface IOpenIddictAuthorizationManager
///
/// The date before which authorizations are not pruned.
/// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
+ /// The number of authorizations that were removed.
+ ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
///
/// Tries to revoke an authorization.
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index af039c7e..1866a21c 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -406,10 +406,16 @@ public interface IOpenIddictTokenManager
///
/// The date before which tokens are not pruned.
/// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
+ /// The number of tokens that were removed.
+ ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default);
+
+ ///
+ /// Revokes all the tokens associated with the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ /// The number of tokens associated with the specified authorization that were marked as revoked.
+ ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken = default);
///
/// Tries to redeem a token.
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 9fc60a72..e8d130a8 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -2739,6 +2739,12 @@ This may indicate that the hashed entry is corrupted or malformed.
The request was rejected because the '{Method}' client authentication method that was used by the client application is not enabled in the server options.
+
+ {Count} tokens associated with the authorization '{Identifier}' were revoked to prevent a potential token replay attack.
+
+
+ An error occurred while trying to revoke the tokens associated with the authorization '{Identifier}'.
+
https://documentation.openiddict.com/errors/{0}
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
index d766517c..bb5e122b 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
@@ -276,8 +276,8 @@ public interface IOpenIddictAuthorizationStore where TAuthorizat
///
/// The date before which authorizations are not pruned.
/// The that can be used to abort the operation.
- /// A that can be used to monitor the asynchronous operation.
- ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken);
+ /// The number of authorizations that were removed.
+ ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken);
///
/// Sets the application identifier associated with an authorization.
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
index 0cbfd1cb..dfb6af73 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
@@ -323,8 +323,16 @@ public interface IOpenIddictTokenStore where TToken : class
///
/// The date before which tokens are not pruned.
/// The that can be used to abort the operation.
- /// A that can be used to monitor the asynchronous operation.
- ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken);
+ /// The number of tokens that were removed.
+ ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken);
+
+ ///
+ /// Revokes all the tokens associated with the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ /// The number of tokens associated with the specified authorization that were marked as revoked.
+ ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken);
///
/// Sets the application identifier associated with a token.
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 7cd65946..b3f5cfb3 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -1020,10 +1020,8 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori
///
/// The date before which authorizations are not pruned.
/// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
+ /// The number of authorizations that were removed.
+ public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
=> Store.PruneAsync(threshold, cancellationToken);
///
@@ -1332,7 +1330,7 @@ public class OpenIddictAuthorizationManager : IOpenIddictAuthori
=> PopulateAsync((TAuthorization) authorization, descriptor, cancellationToken);
///
- ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ ValueTask IOpenIddictAuthorizationManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
=> PruneAsync(threshold, cancellationToken);
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index a0ebd3cd..26a8cb6f 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -1051,11 +1051,26 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok
///
/// The date before which tokens are not pruned.
/// The that can be used to abort the operation.
- ///
- /// A that can be used to monitor the asynchronous operation.
- ///
- public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
+ /// The number of tokens that were removed.
+ public virtual ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken = default)
=> Store.PruneAsync(threshold, cancellationToken);
+
+ ///
+ /// Revokes all the tokens associated with the specified authorization identifier.
+ ///
+ /// The authorization identifier associated with the tokens.
+ /// The that can be used to abort the operation.
+ /// The number of tokens associated with the specified authorization that were marked as revoked.
+ public virtual ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
+ }
+
+ return Store.RevokeByAuthorizationIdAsync(identifier, cancellationToken);
+ }
+
///
/// Tries to redeem a token.
///
@@ -1479,9 +1494,13 @@ public class OpenIddictTokenManager : IOpenIddictTokenManager where TTok
=> PopulateAsync((TToken) token, descriptor, cancellationToken);
///
- ValueTask IOpenIddictTokenManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ ValueTask IOpenIddictTokenManager.PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
=> PruneAsync(threshold, cancellationToken);
+ ///
+ ValueTask IOpenIddictTokenManager.RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ => RevokeByAuthorizationIdAsync(identifier, cancellationToken);
+
///
ValueTask IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken)
=> TryRedeemAsync((TToken) token, cancellationToken);
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
index 15567df7..771c1ae9 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkAuthorizationStore.cs
@@ -591,7 +591,7 @@ public class OpenIddictEntityFrameworkAuthorizationStore
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
// Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing
// entities in a single command without having to retrieve and materialize them first.
@@ -599,6 +599,8 @@ public class OpenIddictEntityFrameworkAuthorizationStore? exceptions = null;
+ var result = 0L;
+
DbContextTransaction? CreateTransaction()
{
// Note: relational providers like Sqlite are known to lack proper support
@@ -662,13 +664,19 @@ public class OpenIddictEntityFrameworkAuthorizationStore
diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
index f5628ed4..d48e6ff8 100644
--- a/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
+++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictEntityFrameworkTokenStore.cs
@@ -574,7 +574,7 @@ public class OpenIddictEntityFrameworkTokenStore
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
// Note: Entity Framework 6.x doesn't support set-based deletes, which prevents removing
// entities in a single command without having to retrieve and materialize them first.
@@ -582,6 +582,8 @@ public class OpenIddictEntityFrameworkTokenStore? exceptions = null;
+ var result = 0L;
+
DbContextTransaction? CreateTransaction()
{
// Note: relational providers like Sqlite are known to lack proper support
@@ -642,13 +644,66 @@ public class OpenIddictEntityFrameworkTokenStore
+ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
+ }
+
+ var key = ConvertIdentifierFromString(identifier);
+
+ List? exceptions = null;
+
+ var result = 0L;
+
+ foreach (var token in await (from token in Tokens
+ where token.Authorization!.Id!.Equals(key)
+ select token).ToListAsync(cancellationToken))
+ {
+ token.Status = Statuses.Revoked;
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ }
+
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(token).State = EntityState.Unchanged;
+
+ exceptions ??= [];
+ exceptions.Add(exception);
+
+ continue;
}
+
+ result++;
}
if (exceptions is not null)
{
throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions);
}
+
+ return result;
}
///
diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
index ed741dcc..052b0009 100644
--- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
+++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreBuilder.cs
@@ -47,6 +47,16 @@ public sealed class OpenIddictEntityFrameworkCoreBuilder
return this;
}
+ ///
+ /// Prevents the Entity Framework Core stores from using bulk operations.
+ ///
+ ///
+ /// Note: bulk operations are only supported when targeting .NET 7.0 and higher.
+ ///
+ /// The instance.
+ public OpenIddictEntityFrameworkCoreBuilder DisableBulkOperations()
+ => Configure(options => options.DisableBulkOperations = true);
+
///
/// Configures OpenIddict to use the default OpenIddict
/// Entity Framework Core entities, with the specified key type.
diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
index 60f179b9..3151486c 100644
--- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
+++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreHelpers.cs
@@ -4,10 +4,13 @@
* the license and the contributors participating to this project.
*/
+using System;
+using System.Data;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.EntityFrameworkCore;
using OpenIddict.EntityFrameworkCore.Models;
+using OpenIddict.Extensions;
namespace Microsoft.EntityFrameworkCore;
@@ -224,4 +227,40 @@ public static class OpenIddictEntityFrameworkCoreHelpers
#endif
}
}
+
+ ///
+ /// Tries to create a new with the specified .
+ ///
+ /// The Entity Framework Core context.
+ /// The desired level of isolation.
+ /// The that can be used to abort the operation.
+ /// The if it could be created, otherwise.
+ internal static async ValueTask CreateTransactionAsync(
+ this DbContext context, IsolationLevel level, CancellationToken cancellationToken)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ // Note: transactions that specify an explicit isolation level are only supported by
+ // relational providers and trying to use them with a different provider results in
+ // an invalid operation exception being thrown at runtime. To prevent that, a manual
+ // check is made to ensure the underlying transaction manager is relational.
+ var manager = context.Database.GetService();
+ if (manager is IRelationalTransactionManager)
+ {
+ try
+ {
+ return await context.Database.BeginTransactionAsync(level, cancellationToken);
+ }
+
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ return null;
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs
index 7bcdbef6..24a37b46 100644
--- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs
+++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreOptions.cs
@@ -18,4 +18,12 @@ public sealed class OpenIddictEntityFrameworkCoreOptions
/// an exception is thrown at runtime when trying to use the stores.
///
public Type? DbContextType { get; set; }
+
+ ///
+ /// Gets or sets a boolean indicating whether bulk operations should be disabled.
+ ///
+ ///
+ /// Note: bulk operations are only supported when targeting .NET 7.0 and higher.
+ ///
+ public bool DisableBulkOperations { get; set; }
}
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
index ba747c10..02fa0de1 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreApplicationStore.cs
@@ -9,6 +9,7 @@ using System.ComponentModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Encodings.Web;
@@ -17,7 +18,6 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.EntityFrameworkCore.Models;
-using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
namespace OpenIddict.EntityFrameworkCore;
@@ -153,101 +153,129 @@ public class OpenIddictEntityFrameworkCoreApplicationStore CreateTransactionAsync()
+#if SUPPORTS_BULK_DBSET_OPERATIONS
+ if (!Options.CurrentValue.DisableBulkOperations)
{
- // Note: transactions that specify an explicit isolation level are only supported by
- // relational providers and trying to use them with a different provider results in
- // an invalid operation exception being thrown at runtime. To prevent that, a manual
- // check is made to ensure the underlying transaction manager is relational.
- var manager = Context.Database.GetService();
- if (manager is IRelationalTransactionManager)
+ var strategy = Context.Database.CreateExecutionStrategy();
+ await strategy.ExecuteAsync(async () =>
{
+ // To prevent an SQL exception from being thrown if a new associated entity is
+ // created after the existing entries have been listed, the following logic is
+ // executed in a serializable transaction, that will lock the affected tables.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+
+ // Remove all the tokens associated with the application.
+ await (from token in Tokens
+ where token.Application!.Id!.Equals(application.Id)
+ select token).ExecuteDeleteAsync(cancellationToken);
+
+ // Remove all the authorizations associated with the application and
+ // the tokens attached to these implicit or explicit authorizations.
+ await (from authorization in Authorizations
+ where authorization.Application!.Id!.Equals(application.Id)
+ select authorization).ExecuteDeleteAsync(cancellationToken);
+
+ // Note: calling DbContext.SaveChangesAsync() is not necessary
+ // with bulk delete operations as they are executed immediately.
+
+ Context.Remove(application);
+
try
{
- return await Context.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
}
- catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ catch (DbUpdateConcurrencyException exception)
{
- return null;
- }
- }
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(application).State = EntityState.Unchanged;
- return null;
+ throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
+ }
+ });
}
- // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be
- // filtered using authorization.Application.Id.Equals(key). To work around this issue,
- // this local method uses an explicit join before applying the equality check.
- // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
-
- Task> ListAuthorizationsAsync()
- => (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking()
- join element in Applications.AsTracking() on authorization.Application!.Id equals element.Id
- where element.Id!.Equals(application.Id)
- select authorization).ToListAsync(cancellationToken);
-
- // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
- // filtered using token.Application.Id.Equals(key). To work around this issue,
- // this local method uses an explicit join before applying the equality check.
- // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
-
- Task> ListTokensAsync()
- => (from token in Tokens.AsTracking()
- where token.Authorization == null
- join element in Applications.AsTracking() on token.Application!.Id equals element.Id
- where element.Id!.Equals(application.Id)
- select token).ToListAsync(cancellationToken);
-
- // To prevent an SQL exception from being thrown if a new associated entity is
- // created after the existing entries have been listed, the following logic is
- // executed in a serializable transaction, that will lock the affected tables.
- using var transaction = await CreateTransactionAsync();
-
- // Remove all the authorizations associated with the application and
- // the tokens attached to these implicit or explicit authorizations.
- var authorizations = await ListAuthorizationsAsync();
- foreach (var authorization in authorizations)
- {
- foreach (var token in authorization.Tokens)
+ else
+#endif
+ {
+ // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be
+ // filtered using authorization.Application.Id.Equals(key). To work around this issue,
+ // this local method uses an explicit join before applying the equality check.
+ // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
+
+ Task> ListAuthorizationsAsync()
+ => (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking()
+ join element in Applications.AsTracking() on authorization.Application!.Id equals element.Id
+ where element.Id!.Equals(application.Id)
+ select authorization).ToListAsync(cancellationToken);
+
+ // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
+ // filtered using token.Application.Id.Equals(key). To work around this issue,
+ // this local method uses an explicit join before applying the equality check.
+ // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
+
+ Task> ListTokensAsync()
+ => (from token in Tokens.AsTracking()
+ where token.Authorization == null
+ join element in Applications.AsTracking() on token.Application!.Id equals element.Id
+ where element.Id!.Equals(application.Id)
+ select token).ToListAsync(cancellationToken);
+
+ var strategy = Context.Database.CreateExecutionStrategy();
+ await strategy.ExecuteAsync(async () =>
{
- Context.Remove(token);
- }
+ // To prevent an SQL exception from being thrown if a new associated entity is
+ // created after the existing entries have been listed, the following logic is
+ // executed in a serializable transaction, that will lock the affected tables.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+
+ // Remove all the authorizations associated with the application and
+ // the tokens attached to these implicit or explicit authorizations.
+ var authorizations = await ListAuthorizationsAsync();
+ foreach (var authorization in authorizations)
+ {
+ foreach (var token in authorization.Tokens)
+ {
+ Context.Remove(token);
+ }
- Context.Remove(authorization);
- }
+ Context.Remove(authorization);
+ }
- // Remove all the tokens associated with the application.
- var tokens = await ListTokensAsync();
- foreach (var token in tokens)
- {
- Context.Remove(token);
- }
+ // Remove all the tokens associated with the application.
+ var tokens = await ListTokensAsync();
+ foreach (var token in tokens)
+ {
+ Context.Remove(token);
+ }
- Context.Remove(application);
+ Context.Remove(application);
- try
- {
- await Context.SaveChangesAsync(cancellationToken);
- transaction?.Commit();
- }
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
- catch (DbUpdateConcurrencyException exception)
- {
- // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
- Context.Entry(application).State = EntityState.Unchanged;
+ catch (DbUpdateConcurrencyException exception)
+ {
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(application).State = EntityState.Unchanged;
- foreach (var authorization in authorizations)
- {
- Context.Entry(authorization).State = EntityState.Unchanged;
- }
+ foreach (var authorization in authorizations)
+ {
+ Context.Entry(authorization).State = EntityState.Unchanged;
+ }
- foreach (var token in tokens)
- {
- Context.Entry(token).State = EntityState.Unchanged;
- }
+ foreach (var token in tokens)
+ {
+ Context.Entry(token).State = EntityState.Unchanged;
+ }
- throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
+ throw new ConcurrencyException(SR.GetResourceString(SR.ID0239), exception);
+ }
+ });
}
}
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
index 31e21d66..69edb1b0 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreAuthorizationStore.cs
@@ -150,71 +150,93 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore CreateTransactionAsync()
- {
- // Note: transactions that specify an explicit isolation level are only supported by
- // relational providers and trying to use them with a different provider results in
- // an invalid operation exception being thrown at runtime. To prevent that, a manual
- // check is made to ensure the underlying transaction manager is relational.
- var manager = Context.Database.GetService();
- if (manager is IRelationalTransactionManager)
+#if SUPPORTS_BULK_DBSET_OPERATIONS
+ if (!Options.CurrentValue.DisableBulkOperations)
+ {
+ var strategy = Context.Database.CreateExecutionStrategy();
+ await strategy.ExecuteAsync(async () =>
{
+ // To prevent an SQL exception from being thrown if a new associated entity is
+ // created after the existing entries have been listed, the following logic is
+ // executed in a serializable transaction, that will lock the affected tables.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+
+ // Remove all the tokens associated with the authorization.
+ await (from token in Tokens.AsTracking()
+ where token.Authorization!.Id!.Equals(authorization.Id)
+ select token).ExecuteDeleteAsync(cancellationToken);
+
+ // Note: calling DbContext.SaveChangesAsync() is not necessary
+ // with bulk delete operations as they are executed immediately.
+
+ Context.Remove(authorization);
+
try
{
- return await Context.Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
}
- catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ catch (DbUpdateConcurrencyException exception)
{
- return null;
- }
- }
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(authorization).State = EntityState.Unchanged;
- return null;
+ throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
+ }
+ });
}
- // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
- // filtered using token.Application.Id.Equals(key). To work around this issue,
- // this local method uses an explicit join before applying the equality check.
- // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
+ else
+#endif
+ {
+ // Note: due to a bug in Entity Framework Core's query visitor, the tokens can't be
+ // filtered using token.Application.Id.Equals(key). To work around this issue,
+ // this local method uses an explicit join before applying the equality check.
+ // See https://github.com/openiddict/openiddict-core/issues/499 for more information.
- Task> ListTokensAsync()
- => (from token in Tokens.AsTracking()
- join element in Authorizations.AsTracking() on token.Authorization!.Id equals element.Id
- where element.Id!.Equals(authorization.Id)
- select token).ToListAsync(cancellationToken);
+ Task> ListTokensAsync()
+ => (from token in Tokens.AsTracking()
+ join element in Authorizations.AsTracking() on token.Authorization!.Id equals element.Id
+ where element.Id!.Equals(authorization.Id)
+ select token).ToListAsync(cancellationToken);
- // To prevent an SQL exception from being thrown if a new associated entity is
- // created after the existing entries have been listed, the following logic is
- // executed in a serializable transaction, that will lock the affected tables.
- using var transaction = await CreateTransactionAsync();
+ var strategy = Context.Database.CreateExecutionStrategy();
+ await strategy.ExecuteAsync(async () =>
+ {
+ // To prevent an SQL exception from being thrown if a new associated entity is
+ // created after the existing entries have been listed, the following logic is
+ // executed in a serializable transaction, that will lock the affected tables.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.Serializable, cancellationToken);
+
+ // Remove all the tokens associated with the authorization.
+ var tokens = await ListTokensAsync();
+ foreach (var token in tokens)
+ {
+ Context.Remove(token);
+ }
- // Remove all the tokens associated with the authorization.
- var tokens = await ListTokensAsync();
- foreach (var token in tokens)
- {
- Context.Remove(token);
- }
+ Context.Remove(authorization);
- Context.Remove(authorization);
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
- try
- {
- await Context.SaveChangesAsync(cancellationToken);
- transaction?.Commit();
- }
+ catch (DbUpdateConcurrencyException exception)
+ {
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(authorization).State = EntityState.Unchanged;
- catch (DbUpdateConcurrencyException exception)
- {
- // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
- Context.Entry(authorization).State = EntityState.Unchanged;
+ foreach (var token in tokens)
+ {
+ Context.Entry(token).State = EntityState.Unchanged;
+ }
- foreach (var token in tokens)
- {
- Context.Entry(token).State = EntityState.Unchanged;
- }
-
- throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
+ throw new ConcurrencyException(SR.GetResourceString(SR.ID0241), exception);
+ }
+ });
}
}
@@ -659,39 +681,17 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
- // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing
- // entities in a single command without having to retrieve and materialize them first.
- // To work around this limitation, entities are manually listed and deleted using a batch logic.
-
List? exceptions = null;
- async ValueTask CreateTransactionAsync()
- {
- // Note: transactions that specify an explicit isolation level are only supported by
- // relational providers and trying to use them with a different provider results in
- // an invalid operation exception being thrown at runtime. To prevent that, a manual
- // check is made to ensure the underlying transaction manager is relational.
- var manager = Context.Database.GetService();
- if (manager is IRelationalTransactionManager)
- {
- // Note: relational providers like Sqlite are known to lack proper support
- // for repeatable read transactions. To ensure this method can be safely used
- // with such providers, the database transaction is created in a try/catch block.
- try
- {
- return await Context.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
- }
+ var result = 0L;
- catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
- {
- return null;
- }
- }
-
- return null;
- }
+ // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable
+ // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime.
+ // To work around this limitation, the threshold represented as a DateTimeOffset
+ // instance is manually converted to a UTC DateTime instance outside the query.
+ var date = threshold.UtcDateTime;
// Note: to avoid sending too many queries, the maximum number of elements
// that can be removed by a single call to PruneAsync() is deliberately limited.
@@ -699,47 +699,87 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore authorization.Tokens).AsTracking()
- where authorization.CreationDate < date
- where authorization.Status != Statuses.Valid ||
- (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any())
- orderby authorization.Id
- select authorization).Take(1_000).ToListAsync(cancellationToken);
-
- if (authorizations.Count is 0)
+#if SUPPORTS_BULK_DBSET_OPERATIONS
+ if (!Options.CurrentValue.DisableBulkOperations)
{
- break;
- }
-
- // Note: new tokens may be attached after the authorizations were retrieved
- // from the database since the transaction level is deliberately limited to
- // repeatable read instead of serializable for performance reasons). In this
- // case, the operation will fail, which is considered an acceptable risk.
- Context.RemoveRange(authorizations);
+ try
+ {
+ var count = await
+ (from authorization in Authorizations
+ where authorization.CreationDate < date
+ where authorization.Status != Statuses.Valid ||
+ (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any())
+ orderby authorization.Id
+ select authorization).Take(1_000).ExecuteDeleteAsync(cancellationToken);
+
+ if (count is 0)
+ {
+ break;
+ }
+
+ // Note: calling DbContext.SaveChangesAsync() is not necessary
+ // with bulk delete operations as they are executed immediately.
+
+ result += count;
+ }
- try
- {
- await Context.SaveChangesAsync(cancellationToken);
- transaction?.Commit();
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ exceptions ??= new List(capacity: 1);
+ exceptions.Add(exception);
+ }
}
- catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ else
+#endif
{
- exceptions ??= [];
- exceptions.Add(exception);
+ var strategy = Context.Database.CreateExecutionStrategy();
+ var count = await strategy.ExecuteAsync(async () =>
+ {
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // after it was retrieved from the database, the following logic is executed in
+ // a repeatable read transaction, that will put a lock on the retrieved entries
+ // and thus prevent them from being concurrently modified outside this block.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
+
+ var authorizations = await
+ (from authorization in Authorizations.Include(authorization => authorization.Tokens).AsTracking()
+ where authorization.CreationDate < date
+ where authorization.Status != Statuses.Valid ||
+ (authorization.Type == AuthorizationTypes.AdHoc && !authorization.Tokens.Any())
+ orderby authorization.Id
+ select authorization).Take(1_000).ToListAsync(cancellationToken);
+
+ if (authorizations.Count is not 0)
+ {
+ // Note: new tokens may be attached after the authorizations were retrieved
+ // from the database since the transaction level is deliberately limited to
+ // repeatable read instead of serializable for performance reasons). In this
+ // case, the operation will fail, which is considered an acceptable risk.
+ Context.RemoveRange(authorizations);
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ exceptions ??= [];
+ exceptions.Add(exception);
+ }
+ }
+
+ return authorizations.Count;
+ });
+
+ if (count is 0)
+ {
+ break;
+ }
+
+ result += count;
}
}
@@ -747,6 +787,8 @@ public class OpenIddictEntityFrameworkCoreAuthorizationStore
diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
index 89673ca9..cf5920e8 100644
--- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
+++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictEntityFrameworkCoreTokenStore.cs
@@ -624,91 +624,172 @@ public class OpenIddictEntityFrameworkCoreTokenStore
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
- // Note: Entity Framework Core doesn't support set-based deletes, which prevents removing
- // entities in a single command without having to retrieve and materialize them first.
- // To work around this limitation, entities are manually listed and deleted using a batch logic.
-
List? exceptions = null;
- async ValueTask CreateTransactionAsync()
+ var result = 0L;
+
+ // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable
+ // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime.
+ // To work around this limitation, the threshold represented as a DateTimeOffset
+ // instance is manually converted to a UTC DateTime instance outside the query.
+ var date = threshold.UtcDateTime;
+
+ // Note: to avoid sending too many queries, the maximum number of elements
+ // that can be removed by a single call to PruneAsync() is deliberately limited.
+ for (var index = 0; index < 1_000; index++)
{
- // Note: transactions that specify an explicit isolation level are only supported by
- // relational providers and trying to use them with a different provider results in
- // an invalid operation exception being thrown at runtime. To prevent that, a manual
- // check is made to ensure the underlying transaction manager is relational.
- var manager = Context.Database.GetService();
- if (manager is IRelationalTransactionManager)
+ cancellationToken.ThrowIfCancellationRequested();
+
+#if SUPPORTS_BULK_DBSET_OPERATIONS
+ if (!Options.CurrentValue.DisableBulkOperations)
{
- // Note: relational providers like Sqlite are known to lack proper support
- // for repeatable read transactions. To ensure this method can be safely used
- // with such providers, the database transaction is created in a try/catch block.
try
{
- return await Context.Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
+ var count = await
+ (from token in Tokens
+ where token.CreationDate < date
+ where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) ||
+ (token.Authorization != null && token.Authorization.Status != Statuses.Valid) ||
+ token.ExpirationDate < DateTime.UtcNow
+ orderby token.Id
+ select token).Take(1_000).ExecuteDeleteAsync(cancellationToken);
+
+ if (count is 0)
+ {
+ break;
+ }
+
+ // Note: calling DbContext.SaveChangesAsync() is not necessary
+ // with bulk delete operations as they are executed immediately.
+
+ result += count;
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
- return null;
+ exceptions ??= new List(capacity: 1);
+ exceptions.Add(exception);
}
}
- return null;
+ else
+#endif
+ {
+ var strategy = Context.Database.CreateExecutionStrategy();
+ var count = await strategy.ExecuteAsync(async () =>
+ {
+ // To prevent concurrency exceptions from being thrown if an entry is modified
+ // after it was retrieved from the database, the following logic is executed in
+ // a repeatable read transaction, that will put a lock on the retrieved entries
+ // and thus prevent them from being concurrently modified outside this block.
+ using var transaction = await Context.CreateTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken);
+
+ var tokens = await
+ (from token in Tokens.AsTracking()
+ where token.CreationDate < date
+ where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) ||
+ (token.Authorization != null && token.Authorization.Status != Statuses.Valid) ||
+ token.ExpirationDate < DateTime.UtcNow
+ orderby token.Id
+ select token).Take(1_000).ToListAsync(cancellationToken);
+
+ if (tokens.Count is not 0)
+ {
+ Context.RemoveRange(tokens);
+
+ try
+ {
+ await Context.SaveChangesAsync(cancellationToken);
+ transaction?.Commit();
+ }
+
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ exceptions ??= [];
+ exceptions.Add(exception);
+ }
+ }
+
+ return tokens.Count;
+ });
+
+ if (count is 0)
+ {
+ break;
+ }
+
+ result += count;
+ }
}
- // Note: to avoid sending too many queries, the maximum number of elements
- // that can be removed by a single call to PruneAsync() is deliberately limited.
- for (var index = 0; index < 1_000; index++)
+ if (exceptions is not null)
{
- cancellationToken.ThrowIfCancellationRequested();
+ throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions);
+ }
- // To prevent concurrency exceptions from being thrown if an entry is modified
- // after it was retrieved from the database, the following logic is executed in
- // a repeatable read transaction, that will put a lock on the retrieved entries
- // and thus prevent them from being concurrently modified outside this block.
- using var transaction = await CreateTransactionAsync();
-
- // Note: the Oracle MySQL provider doesn't support DateTimeOffset and is unable
- // to create a SQL query with an expression calling DateTimeOffset.UtcDateTime.
- // To work around this limitation, the threshold represented as a DateTimeOffset
- // instance is manually converted to a UTC DateTime instance outside the query.
- var date = threshold.UtcDateTime;
-
- var tokens = await
- (from token in Tokens.AsTracking()
- where token.CreationDate < date
- where (token.Status != Statuses.Inactive && token.Status != Statuses.Valid) ||
- (token.Authorization != null && token.Authorization.Status != Statuses.Valid) ||
- token.ExpirationDate < DateTime.UtcNow
- orderby token.Id
- select token).Take(1_000).ToListAsync(cancellationToken);
-
- if (tokens.Count is 0)
- {
- break;
- }
+ return result;
+ }
+
+ ///
+ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
+ }
+
+ var key = ConvertIdentifierFromString(identifier);
- Context.RemoveRange(tokens);
+#if SUPPORTS_BULK_DBSET_OPERATIONS
+ if (!Options.CurrentValue.DisableBulkOperations)
+ {
+ return await (
+ from token in Tokens
+ where token.Authorization!.Id!.Equals(key)
+ select token).ExecuteUpdateAsync(entity => entity.SetProperty(
+ token => token.Status, Statuses.Revoked), cancellationToken);
+
+ // Note: calling DbContext.SaveChangesAsync() is not necessary
+ // with bulk update operations as they are executed immediately.
+ }
+#endif
+ List? exceptions = null;
+
+ var result = 0L;
+
+ foreach (var token in await (from token in Tokens
+ where token.Authorization!.Id!.Equals(key)
+ select token).ToListAsync(cancellationToken))
+ {
+ token.Status = Statuses.Revoked;
try
{
await Context.SaveChangesAsync(cancellationToken);
- transaction?.Commit();
}
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
+ // Reset the state of the entity to prevents future calls to SaveChangesAsync() from failing.
+ Context.Entry(token).State = EntityState.Unchanged;
+
exceptions ??= [];
exceptions.Add(exception);
+
+ continue;
}
+
+ result++;
}
if (exceptions is not null)
{
throw new AggregateException(SR.GetResourceString(SR.ID0249), exceptions);
}
+
+ return result;
}
///
diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs
index bd6bb46f..233d1804 100644
--- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs
+++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbAuthorizationStore.cs
@@ -10,6 +10,7 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Options;
+using OpenIddict.Extensions;
using OpenIddict.MongoDb.Models;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@@ -518,11 +519,13 @@ public class OpenIddictMongoDbAuthorizationStore : IOpenIddictAu
}
///
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName);
+ var result = 0L;
+
// 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.
@@ -538,33 +541,12 @@ public class OpenIddictMongoDbAuthorizationStore : IOpenIddictAu
// 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 deliberately limited.
- foreach (var buffer in Buffer(identifiers.Take(1_000_000), 1_000))
+ foreach (var buffer in identifiers.Take(1_000_000).Buffer(1_000))
{
- await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id), cancellationToken);
+ result += (await collection.DeleteManyAsync(authorization => buffer.Contains(authorization.Id), cancellationToken)).DeletedCount;
}
- static IEnumerable> Buffer(IEnumerable source, int count)
- {
- List? buffer = null;
-
- foreach (var element in source)
- {
- buffer ??= [];
- buffer.Add(element);
-
- if (buffer.Count == count)
- {
- yield return buffer;
-
- buffer = null;
- }
- }
-
- if (buffer is not null)
- {
- yield return buffer;
- }
- }
+ return result;
}
///
diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
index 6310090a..70858b3b 100644
--- a/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
+++ b/src/OpenIddict.MongoDb/Stores/OpenIddictMongoDbTokenStore.cs
@@ -10,6 +10,7 @@ using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using Microsoft.Extensions.Options;
+using OpenIddict.Extensions;
using OpenIddict.MongoDb.Models;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@@ -554,11 +555,13 @@ public class OpenIddictMongoDbTokenStore : IOpenIddictTokenStore
}
///
- public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
+ public virtual async ValueTask PruneAsync(DateTimeOffset threshold, CancellationToken cancellationToken)
{
var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName);
+ var result = 0L;
+
// Note: directly deleting the resulting set of an aggregate query is not supported by MongoDB.
// To work around this limitation, the token identifiers are stored in an intermediate list
// and delete requests are sent to remove the documents corresponding to these identifiers.
@@ -575,33 +578,30 @@ public class OpenIddictMongoDbTokenStore : IOpenIddictTokenStore
// 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 deliberately limited.
- foreach (var buffer in Buffer(identifiers.Take(1_000_000), 1_000))
+ foreach (var buffer in identifiers.Take(1_000_000).Buffer(1_000))
{
- await collection.DeleteManyAsync(token => buffer.Contains(token.Id), cancellationToken);
+ result += (await collection.DeleteManyAsync(token => buffer.Contains(token.Id), cancellationToken)).DeletedCount;
}
- static IEnumerable> Buffer(IEnumerable source, int count)
- {
- List? buffer = null;
-
- foreach (var element in source)
- {
- buffer ??= [];
- buffer.Add(element);
+ return result;
+ }
- if (buffer.Count == count)
- {
- yield return buffer;
+ ///
+ public virtual async ValueTask RevokeByAuthorizationIdAsync(string identifier, CancellationToken cancellationToken)
+ {
+ if (string.IsNullOrEmpty(identifier))
+ {
+ throw new ArgumentException(SR.GetResourceString(SR.ID0195), nameof(identifier));
+ }
- buffer = null;
- }
- }
+ var database = await Context.GetDatabaseAsync(cancellationToken);
+ var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName);
- if (buffer is not null)
- {
- yield return buffer;
- }
- }
+ return (await collection.UpdateManyAsync(
+ filter : token => token.AuthorizationId == ObjectId.Parse(identifier),
+ update : Builders.Update.Set(token => token.Status, Statuses.Revoked),
+ options : null,
+ cancellationToken: cancellationToken)).MatchedCount;
}
///
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
index f7863f76..350eed97 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs
@@ -962,7 +962,8 @@ public static partial class OpenIddictServerHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0021));
// If the token is already marked as redeemed, this may indicate that it was compromised.
- // In this case, revoke the entire chain of tokens associated with the authorization.
+ // In this case, revoke the entire chain of tokens associated with the authorization, if one was attached to the token.
+ //
// Special logic is used to avoid revoking refresh tokens already marked as redeemed to allow for a small leeway.
// Note: the authorization itself is not revoked to allow the legitimate client to start a new flow.
// See https://tools.ietf.org/html/rfc6749#section-10.5 for more information.
@@ -970,6 +971,26 @@ public static partial class OpenIddictServerHandlers
{
if (!context.Principal.HasTokenType(TokenTypeHints.RefreshToken) || !await IsReusableAsync(token))
{
+ if (!string.IsNullOrEmpty(context.AuthorizationId))
+ {
+ long? count = null;
+
+ try
+ {
+ count = await _tokenManager.RevokeByAuthorizationIdAsync(context.AuthorizationId);
+ }
+
+ catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
+ {
+ context.Logger.LogWarning(exception, SR.GetResourceString(SR.ID6229), context.AuthorizationId);
+ }
+
+ if (count is not null)
+ {
+ context.Logger.LogWarning(SR.GetResourceString(SR.ID6228), count, context.AuthorizationId);
+ }
+ }
+
context.Logger.LogInformation(SR.GetResourceString(SR.ID6002), context.TokenId);
context.Reject(
@@ -996,9 +1017,6 @@ public static partial class OpenIddictServerHandlers
_ => SR.FormatID8000(SR.ID2013)
});
- // Revoke all the token entries associated with the authorization.
- await TryRevokeChainAsync(context.AuthorizationId);
-
return;
}
@@ -1079,34 +1097,6 @@ public static partial class OpenIddictServerHandlers
return false;
}
-
- async ValueTask TryRevokeChainAsync(string? identifier)
- {
- if (string.IsNullOrEmpty(identifier))
- {
- return;
- }
-
- // Revoke all the token entries associated with the authorization,
- // including the redeemed token that was used in the token request.
-
- // Note: the tokens are deliberately buffered before being marked
- // as revoked to prevent issues with providers that try to reuse the
- // connection opened to iterate the tokens instead of opening a new one.
- //
- // See https://github.com/openiddict/openiddict-core/issues/1658 for more information.
- List