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(); + + async Task> IOpenIddictTokenManager.FindAsync(string subject, string client, string status, CancellationToken cancellationToken) + => (await FindAsync(subject, client, status, cancellationToken)).CastArray(); + + async Task> IOpenIddictTokenManager.FindAsync(string subject, string client, string status, string type, CancellationToken cancellationToken) + => (await FindAsync(subject, client, status, type, cancellationToken)).CastArray(); + async Task> IOpenIddictTokenManager.FindByApplicationIdAsync(string identifier, CancellationToken cancellationToken) => (await FindByApplicationIdAsync(identifier, cancellationToken)).CastArray(); diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs index 11723500..73e7db65 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -334,6 +334,50 @@ namespace OpenIddict.EntityFramework select authorization).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, + ImmutableArray scopes, CancellationToken cancellationToken) + { + var authorizations = await FindAsync(subject, client, status, type, cancellationToken); + if (authorizations.IsEmpty) + { + return ImmutableArray.Create(); + } + + var builder = ImmutableArray.CreateBuilder(authorizations.Length); + + foreach (var authorization in authorizations) + { + async Task HasScopesAsync() + => (await GetScopesAsync(authorization, cancellationToken)) + .ToImmutableHashSet(StringComparer.Ordinal) + .IsSupersetOf(scopes); + + if (await HasScopesAsync()) + { + builder.Add(authorization); + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); + } + /// /// Retrieves an authorization using its unique identifier. /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs index 11ae518c..33db32b3 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -180,6 +180,129 @@ namespace OpenIddict.EntityFramework } } + /// + /// 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) + { + 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 cannot be null or empty.", nameof(client)); + } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Application != null && + token.Application.Id.Equals(key) && + token.Subject == subject + select token).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, CancellationToken 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 cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Application != null && + token.Application.Id.Equals(key) && + token.Subject == subject && + token.Status == status + select token).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, CancellationToken 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)); + } + + var key = ConvertIdentifierFromString(client); + + return ImmutableArray.CreateRange( + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + where token.Application != null && + token.Application.Id.Equals(key) && + token.Subject == subject && + token.Status == status && + token.Type == type + select token).ToListAsync(cancellationToken)); + } + /// /// Retrieves the list of tokens corresponding to the specified application identifier. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs index f78a5292..477d6b9a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs @@ -39,6 +39,12 @@ namespace OpenIddict.EntityFrameworkCore builder.HasKey(authorization => authorization.Id); + builder.HasIndex("ApplicationId", + nameof(OpenIddictAuthorization.Scopes), + nameof(OpenIddictAuthorization.Status), + nameof(OpenIddictAuthorization.Subject), + nameof(OpenIddictAuthorization.Type)); + builder.Property(authorization => authorization.ConcurrencyToken) .IsConcurrencyToken(); diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs index c3665e70..daa1dbee 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs +++ b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs @@ -42,6 +42,11 @@ namespace OpenIddict.EntityFrameworkCore builder.HasIndex(token => token.ReferenceId) .IsUnique(); + builder.HasIndex("ApplicationId", + nameof(OpenIddictToken.Status), + nameof(OpenIddictToken.Subject), + nameof(OpenIddictToken.Type)); + builder.Property(token => token.ConcurrencyToken) .IsConcurrencyToken(); diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index f3309daa..454fd10a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -405,6 +405,50 @@ namespace OpenIddict.EntityFrameworkCore ConvertIdentifierFromString(client), subject, status, type).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, + ImmutableArray scopes, CancellationToken cancellationToken) + { + var authorizations = await FindAsync(subject, client, status, type, cancellationToken); + if (authorizations.IsEmpty) + { + return ImmutableArray.Create(); + } + + var builder = ImmutableArray.CreateBuilder(authorizations.Length); + + foreach (var authorization in authorizations) + { + async Task HasScopesAsync() + => (await GetScopesAsync(authorization, cancellationToken)) + .ToImmutableHashSet(StringComparer.Ordinal) + .IsSupersetOf(scopes); + + if (await HasScopesAsync()) + { + builder.Add(authorization); + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); + } + /// /// Exposes a compiled query allowing to retrieve an authorization using its unique identifier. /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 5df6a271..5148a370 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -202,6 +202,165 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + /// Exposes a compiled query allowing to retrieve the tokens corresponding + /// to the specified subject and associated with the application identifier. + /// + private static readonly Func> FindBySubjectAndClient = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be + // filtered using token.Application.Id.Equals(key). To work around this issue, + // this compiled query uses an explicit join before applying the equality check. + // See https://github.com/openiddict/openiddict-core/issues/499 for more information. + EF.CompileAsyncQuery((TContext context, TKey identifier, string subject) => + from token in context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization) + .AsTracking() + where token.Subject == subject + join application in context.Set().AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(identifier) + select token); + + /// + /// 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) + { + 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 cannot be null or empty.", nameof(client)); + } + + return ImmutableArray.CreateRange(await FindBySubjectAndClient(Context, + ConvertIdentifierFromString(client), subject).ToListAsync(cancellationToken)); + } + + /// + /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. + /// + private static readonly Func> FindBySubjectClientAndStatus = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be + // filtered using token.Application.Id.Equals(key). To work around this issue, + // this compiled query uses an explicit join before applying the equality check. + // See https://github.com/openiddict/openiddict-core/issues/499 for more information. + EF.CompileAsyncQuery((TContext context, TKey identifier, string subject, string status) => + from token in context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization) + .AsTracking() + where token.Subject == subject && token.Status == status + join application in context.Set().AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(identifier) + select token); + + /// + /// 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) + { + 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)); + } + + return ImmutableArray.CreateRange(await FindBySubjectClientAndStatus(Context, + ConvertIdentifierFromString(client), subject, status).ToListAsync(cancellationToken)); + } + + /// + /// Exposes a compiled query allowing to retrieve the tokens matching the specified parameters. + /// + private static readonly Func> FindBySubjectClientStatusAndType = + // Note: due to a bug in Entity Framework Core's query visitor, the authorizations can't be + // filtered using token.Application.Id.Equals(key). To work around this issue, + // this compiled query uses an explicit join before applying the equality check. + // See https://github.com/openiddict/openiddict-core/issues/499 for more information. + EF.CompileAsyncQuery((TContext context, TKey identifier, string subject, string status, string type) => + from token in context.Set() + .Include(token => token.Application) + .Include(token => token.Authorization) + .AsTracking() + where token.Subject == subject && + token.Status == status && + token.Type == type + join application in context.Set().AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(identifier) + select token); + + /// + /// 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) + { + 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)); + } + + return ImmutableArray.CreateRange(await FindBySubjectClientStatusAndType(Context, + ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken)); + } + /// /// Exposes a compiled query allowing to retrieve the list of /// tokens corresponding to the specified application identifier. diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs index 69c73a5a..af3aaadf 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs @@ -61,8 +61,8 @@ namespace OpenIddict.MongoDb { throw new InvalidOperationException(new StringBuilder() .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.") - .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ") - .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.") + .Append("Make sure that the MongoDB server is ready and accepts connections from this machine or use ") + .Append("'services.AddOpenIddict().AddCore().UseMongoDb().SetInitializationTimeout()' to adjust the timeout.") .ToString()); } @@ -89,19 +89,43 @@ namespace OpenIddict.MongoDb // Note: the cancellation token passed as a parameter is deliberately not used here to ensure // the cancellation of a single store operation doesn't prevent the indexes from being created. var applications = database.GetCollection(options.ApplicationsCollectionName); - await applications.Indexes.CreateOneAsync(new CreateIndexModel( - Builders.IndexKeys.Ascending(application => application.ClientId), + await applications.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Builders.IndexKeys.Ascending(application => application.ClientId), + new CreateIndexOptions + { + Unique = true + }), + + new CreateIndexModel( + Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris), + new CreateIndexOptions + { + Background = true + }), + + new CreateIndexModel( + Builders.IndexKeys.Ascending(application => application.RedirectUris), + new CreateIndexOptions + { + Background = true + }) + }); + + var authorizations = database.GetCollection(options.AuthorizationsCollectionName); + await authorizations.Indexes.CreateOneAsync(new CreateIndexModel( + Builders.IndexKeys + .Ascending(authorization => authorization.ApplicationId) + .Ascending(authorization => authorization.Scopes) + .Ascending(authorization => authorization.Status) + .Ascending(authorization => authorization.Subject) + .Ascending(authorization => authorization.Type), new CreateIndexOptions { - Unique = true + Background = true })); - await applications.Indexes.CreateOneAsync(new CreateIndexModel( - Builders.IndexKeys.Ascending(application => application.PostLogoutRedirectUris))); - - await applications.Indexes.CreateOneAsync(new CreateIndexModel( - Builders.IndexKeys.Ascending(application => application.RedirectUris))); - var scopes = database.GetCollection(options.ScopesCollectionName); await scopes.Indexes.CreateOneAsync(new CreateIndexModel( Builders.IndexKeys.Ascending(scope => scope.Name), @@ -111,13 +135,27 @@ namespace OpenIddict.MongoDb })); var tokens = database.GetCollection(options.TokensCollectionName); - await tokens.Indexes.CreateOneAsync(new CreateIndexModel( - Builders.IndexKeys.Ascending(token => token.ReferenceId), - new CreateIndexOptions - { - PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId), - Unique = true - })); + await tokens.Indexes.CreateManyAsync(new[] + { + new CreateIndexModel( + Builders.IndexKeys.Ascending(token => token.ReferenceId), + new CreateIndexOptions + { + PartialFilterExpression = Builders.Filter.Exists(token => token.ReferenceId), + Unique = true + }), + + new CreateIndexModel( + Builders.IndexKeys + .Ascending(token => token.ApplicationId) + .Ascending(token => token.Status) + .Ascending(token => token.Subject) + .Ascending(token => token.Type), + new CreateIndexOptions + { + Background = true + }) + }); } return _database = database; diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs index 7274f1cf..dafb7860 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs @@ -7,7 +7,6 @@ using System; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using OpenIddict.MongoDb; using OpenIddict.MongoDb.Models; @@ -83,4 +82,4 @@ namespace Microsoft.Extensions.DependencyInjection return builder; } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs index 64b2d00f..6292807d 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -265,6 +265,55 @@ namespace OpenIddict.MongoDb authorization.Type == type).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, + ImmutableArray scopes, CancellationToken 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)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(authorization => + authorization.Subject == subject && + authorization.ApplicationId == ObjectId.Parse(client) && + authorization.Status == status && + authorization.Type == type && + Enumerable.All(scopes, scope => authorization.Scopes.Contains(scope))).ToListAsync(cancellationToken)); + } + /// /// Retrieves an authorization using its unique identifier. /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs index 34de4e89..0cd99cea 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -144,6 +144,123 @@ namespace OpenIddict.MongoDb } } + /// + /// 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) + { + 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 cannot be null or empty.", nameof(client)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => + token.ApplicationId == ObjectId.Parse(client) && + token.Subject == subject).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, CancellationToken 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 cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => + token.ApplicationId == ObjectId.Parse(client) && + token.Subject == subject && + token.Status == status).ToListAsync(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. + /// + public virtual async Task> FindAsync( + [NotNull] string subject, [NotNull] string client, + [NotNull] string status, [NotNull] string type, CancellationToken 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)); + } + + var database = await Context.GetDatabaseAsync(cancellationToken); + var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); + + return ImmutableArray.CreateRange(await collection.Find(token => + token.ApplicationId == ObjectId.Parse(client) && + token.Subject == subject && + token.Status == status && + token.Type == type).ToListAsync(cancellationToken)); + } + /// /// Retrieves the list of tokens corresponding to the specified application identifier. /// diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs index ec971a2d..3570deef 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -211,7 +211,7 @@ namespace OpenIddict.Server.Internal // Note: make sure the foreach statement iterates on a copy of the ticket // as the property collection is modified when the property is removed. var parameters = GetParameters(context.Request, context.Ticket.Properties); - foreach (var (property, parameter, value) in parameters.ToArray()) + foreach (var (property, parameter, value) in parameters.ToList()) { context.Response.AddParameter(parameter, value); context.Ticket.RemoveProperty(property); diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs index 88ecfcd8..319cbc3e 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs @@ -5,6 +5,7 @@ */ using System; +using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -48,11 +49,11 @@ namespace OpenIddict.MongoDb.Tests var provider = services.BuildServiceProvider(); var manager = new Mock>(); - manager.Setup(mock => mock.CreateOneAsync(It.IsAny>(), It.IsAny(), It.IsAny())) + manager.Setup(mock => mock.CreateManyAsync(It.IsAny>>(), It.IsAny())) .Returns(async delegate { await Task.Delay(TimeSpan.FromMilliseconds(1000)); - return nameof(OpenIddictMongoDbContextTests); + return new[] { string.Empty }; }); var collection = new Mock>(); @@ -84,8 +85,8 @@ namespace OpenIddict.MongoDb.Tests Assert.Equal(new StringBuilder() .AppendLine("The MongoDB database couldn't be initialized within a reasonable timeframe.") - .Append("Make sure that the MongoDB server is ready and accepts connections from this machine ") - .Append("or use 'options.UseMongoDb().SetInitializationTimeout()' to manually adjust the timeout.") + .Append("Make sure that the MongoDB server is ready and accepts connections from this machine or use ") + .Append("'services.AddOpenIddict().AddCore().UseMongoDb().SetInitializationTimeout()' to adjust the timeout.") .ToString(), exception.Message); } @@ -187,6 +188,7 @@ namespace OpenIddict.MongoDb.Tests // Assert database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Never()); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Never()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Never()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Never()); } @@ -212,6 +214,7 @@ namespace OpenIddict.MongoDb.Tests Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); } @@ -255,6 +258,7 @@ namespace OpenIddict.MongoDb.Tests Assert.Same(database.Object, await context.GetDatabaseAsync(CancellationToken.None)); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Exactly(2)); + database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); database.Verify(mock => mock.GetCollection(It.IsAny(), It.IsAny()), Times.Once()); } @@ -265,6 +269,10 @@ namespace OpenIddict.MongoDb.Tests applications.SetupGet(mock => mock.Indexes) .Returns(Mock.Of>()); + var authorizations = new Mock>(); + authorizations.SetupGet(mock => mock.Indexes) + .Returns(Mock.Of>()); + var scopes = new Mock>(); scopes.SetupGet(mock => mock.Indexes) .Returns(Mock.Of>()); @@ -276,6 +284,8 @@ namespace OpenIddict.MongoDb.Tests var database = new Mock(); database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) .Returns(applications.Object); + database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) + .Returns(authorizations.Object); database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny())) .Returns(scopes.Object); database.Setup(mock => mock.GetCollection(It.IsAny(), It.IsAny()))