diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
index a6d0032c..5dbf934c 100644
--- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
+++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
@@ -81,6 +81,51 @@ namespace OpenIddict.Abstractions
///
Task ExtendAsync([NotNull] object token, [CanBeNull] DateTimeOffset? date, CancellationToken cancellationToken = default);
+ ///
+ /// Retrieves the tokens corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the subject/client.
+ ///
+ Task> FindAsync([NotNull] string subject,
+ [NotNull] string client, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken = default);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The token type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken = default);
+
///
/// Retrieves the list of tokens corresponding to the specified application identifier.
///
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
index 81a65523..7e6ee472 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictAuthorizationStore.cs
@@ -107,6 +107,24 @@ namespace OpenIddict.Abstractions
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, [NotNull] string type, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the authorizations matching the specified parameters.
+ ///
+ /// The subject associated with the authorization.
+ /// The client associated with the authorization.
+ /// The authorization status.
+ /// The authorization type.
+ /// The minimal scopes associated with the authorization.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the authorizations corresponding to the criteria.
+ ///
+ Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type,
+ ImmutableArray scopes, CancellationToken cancellationToken);
+
///
/// Retrieves an authorization using its unique identifier.
///
diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
index b4d718e4..1bdbf7c0 100644
--- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
+++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
@@ -60,6 +60,50 @@ namespace OpenIddict.Abstractions
/// A that can be used to monitor the asynchronous operation.
Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken);
+ ///
+ /// Retrieves the tokens corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the subject/client.
+ ///
+ Task> FindAsync([NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken);
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The token type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken);
+
///
/// Retrieves the list of tokens corresponding to the specified application identifier.
///
diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
index 221ca27a..d30a1fd7 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
@@ -865,12 +865,6 @@ namespace OpenIddict.Core
await Store.SetClientSecretAsync(application, secret, cancellationToken);
}
- var results = await ValidateAsync(application, cancellationToken);
- if (results.Any(result => result != ValidationResult.Success))
- {
- throw new ValidationException(results.FirstOrDefault(result => result != ValidationResult.Success), null, application);
- }
-
await UpdateAsync(application, cancellationToken);
}
diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
index 428ba35e..da7312db 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
@@ -279,7 +279,7 @@ namespace OpenIddict.Core
///
public virtual async Task> FindAsync(
[NotNull] string subject, [NotNull] string client,
- [NotNull] string status, CancellationToken cancellationToken)
+ [NotNull] string status, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(subject))
{
@@ -400,17 +400,42 @@ namespace OpenIddict.Core
[NotNull] string status, [NotNull] string type,
ImmutableArray scopes, CancellationToken cancellationToken = default)
{
- var authorizations = await FindAsync(subject, client, status, type, cancellationToken);
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
+ var authorizations = await Store.FindAsync(subject, client, status, type, scopes, cancellationToken);
if (authorizations.IsEmpty)
{
return ImmutableArray.Create();
}
-
+
var builder = ImmutableArray.CreateBuilder(authorizations.Length);
foreach (var authorization in authorizations)
{
- if (await HasScopesAsync(authorization, scopes, cancellationToken))
+ if (string.Equals(await Store.GetSubjectAsync(authorization, cancellationToken), subject, StringComparison.Ordinal)
+ && await HasScopesAsync(authorization, scopes, cancellationToken))
{
builder.Add(authorization);
}
@@ -888,13 +913,6 @@ namespace OpenIddict.Core
if (!string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase))
{
await Store.SetStatusAsync(authorization, OpenIddictConstants.Statuses.Revoked, cancellationToken);
-
- var results = await ValidateAsync(authorization, cancellationToken);
- if (results.Any(result => result != ValidationResult.Success))
- {
- throw new ValidationException(results.FirstOrDefault(result => result != ValidationResult.Success), null, authorization);
- }
-
await UpdateAsync(authorization, cancellationToken);
}
}
diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
index 69eec87f..b8df734a 100644
--- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
+++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
@@ -179,6 +179,171 @@ namespace OpenIddict.Core
await UpdateAsync(token, cancellationToken);
}
+ ///
+ /// Retrieves the tokens corresponding to the specified
+ /// subject and associated with the application identifier.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the subject/client.
+ ///
+ public virtual async Task> FindAsync([NotNull] string subject,
+ [NotNull] string client, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
+ var tokens = await Store.FindAsync(subject, client, cancellationToken);
+ if (tokens.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(tokens.Length);
+
+ foreach (var token in tokens)
+ {
+ if (string.Equals(await Store.GetSubjectAsync(token, cancellationToken), subject, StringComparison.Ordinal))
+ {
+ builder.Add(token);
+ }
+ }
+
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
+ }
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ public virtual async Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
+ var tokens = await Store.FindAsync(subject, client, status, cancellationToken);
+ if (tokens.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(tokens.Length);
+
+ foreach (var token in tokens)
+ {
+ if (string.Equals(await Store.GetSubjectAsync(token, cancellationToken), subject, StringComparison.Ordinal))
+ {
+ builder.Add(token);
+ }
+ }
+
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
+ }
+
+ ///
+ /// Retrieves the tokens matching the specified parameters.
+ ///
+ /// The subject associated with the token.
+ /// The client associated with the token.
+ /// The token status.
+ /// The token type.
+ /// The that can be used to abort the operation.
+ ///
+ /// A that can be used to monitor the asynchronous operation,
+ /// whose result returns the tokens corresponding to the criteria.
+ ///
+ public virtual async Task> FindAsync(
+ [NotNull] string subject, [NotNull] string client,
+ [NotNull] string status, [NotNull] string type, CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrEmpty(subject))
+ {
+ throw new ArgumentException("The subject cannot be null or empty.", nameof(subject));
+ }
+
+ if (string.IsNullOrEmpty(client))
+ {
+ throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
+ }
+
+ if (string.IsNullOrEmpty(status))
+ {
+ throw new ArgumentException("The status cannot be null or empty.", nameof(status));
+ }
+
+ if (string.IsNullOrEmpty(type))
+ {
+ throw new ArgumentException("The type cannot be null or empty.", nameof(type));
+ }
+
+ // SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default.
+ // To ensure a case-sensitive comparison is enforced independently of the database/table/query collation
+ // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
+
+ var tokens = await Store.FindAsync(subject, client, status, type, cancellationToken);
+ if (tokens.IsEmpty)
+ {
+ return ImmutableArray.Create();
+ }
+
+ var builder = ImmutableArray.CreateBuilder(tokens.Length);
+
+ foreach (var token in tokens)
+ {
+ if (string.Equals(await Store.GetSubjectAsync(token, cancellationToken), subject, StringComparison.Ordinal))
+ {
+ builder.Add(token);
+ }
+ }
+
+ return builder.Count == builder.Capacity ?
+ builder.MoveToImmutable() :
+ builder.ToImmutable();
+ }
+
///
/// Retrieves the list of tokens corresponding to the specified application identifier.
///
@@ -985,6 +1150,15 @@ namespace OpenIddict.Core
Task IOpenIddictTokenManager.ExtendAsync(object token, DateTimeOffset? date, CancellationToken cancellationToken)
=> ExtendAsync((TToken) token, date, cancellationToken);
+ async Task> IOpenIddictTokenManager.FindAsync(string subject, string client, CancellationToken cancellationToken)
+ => (await FindAsync(subject, client, cancellationToken)).CastArray