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 ae3c77ca..0306a2c6 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -876,12 +876,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 dddbf561..a4ae113b 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 e3c139fb..6b932b31 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 cd97b52a..945ebaad 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 cdd68ffa..d3772fb3 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 8ba9eeb5..2d07a960 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 43ec6bd3..dd1dffb4 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/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index b7a76ce8..0e856dd6 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -290,7 +290,7 @@ namespace OpenIddict.EntityFrameworkCore var key = ConvertIdentifierFromString(identifier); - return (from application in Applications + return (from application in Applications.AsTracking() where application.Id.Equals(key) select application).FirstOrDefaultAsync(); } @@ -311,7 +311,7 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return (from application in Applications + return (from application in Applications.AsTracking() where application.ClientId == identifier select application).FirstOrDefaultAsync(); } @@ -337,7 +337,7 @@ namespace OpenIddict.EntityFrameworkCore // are retrieved, a second pass is made to ensure only valid elements are returned. // Implementers that use this method in a hot path may want to override this method // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - var applications = await (from application in Applications + var applications = await (from application in Applications.AsTracking() where application.PostLogoutRedirectUris.Contains(address) select application).ToListAsync(cancellationToken); @@ -384,7 +384,7 @@ namespace OpenIddict.EntityFrameworkCore // are retrieved, a second pass is made to ensure only valid elements are returned. // Implementers that use this method in a hot path may want to override this method // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - var applications = await (from application in Applications + var applications = await (from application in Applications.AsTracking() where application.RedirectUris.Contains(address) select application).ToListAsync(cancellationToken); @@ -726,7 +726,7 @@ namespace OpenIddict.EntityFrameworkCore public virtual async Task> ListAsync( [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) { - var query = Applications.OrderBy(application => application.Id).AsQueryable(); + var query = Applications.OrderBy(application => application.Id).AsTracking(); if (offset.HasValue) { diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index 002170dd..cf5f01ce 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -391,6 +391,50 @@ namespace OpenIddict.EntityFrameworkCore Authorizations, Applications, 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(); + } + /// /// Retrieves an authorization using its unique identifier. /// @@ -409,7 +453,7 @@ namespace OpenIddict.EntityFrameworkCore var key = ConvertIdentifierFromString(identifier); - return (from authorization in Authorizations.Include(authorization => authorization.Application) + return (from authorization in Authorizations.Include(authorization => authorization.Application).AsTracking() where authorization.Id.Equals(key) select authorization).FirstOrDefaultAsync(cancellationToken); } @@ -432,7 +476,7 @@ namespace OpenIddict.EntityFrameworkCore } return ImmutableArray.CreateRange( - await (from authorization in Authorizations.Include(authorization => authorization.Application) + await (from authorization in Authorizations.Include(authorization => authorization.Application).AsTracking() where authorization.Subject == subject select authorization).ToListAsync(cancellationToken)); } @@ -461,7 +505,7 @@ namespace OpenIddict.EntityFrameworkCore async Task RetrieveApplicationIdAsync() { IQueryable Query(IQueryable authorizations, TKey key) - => from element in authorizations + => from element in authorizations.AsTracking() where element.Id.Equals(key) && element.Application != null select element.Application.Id; @@ -666,7 +710,7 @@ namespace OpenIddict.EntityFrameworkCore { var query = Authorizations.Include(authorization => authorization.Application) .OrderBy(authorization => authorization.Id) - .AsQueryable(); + .AsTracking(); if (offset.HasValue) { diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index ffa0967e..af4a19f5 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -197,7 +197,7 @@ namespace OpenIddict.EntityFrameworkCore var key = ConvertIdentifierFromString(identifier); - return (from scope in Scopes + return (from scope in Scopes.AsTracking() where scope.Id.Equals(key) select scope).FirstOrDefaultAsync(cancellationToken); } @@ -218,7 +218,7 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); } - return (from scope in Scopes + return (from scope in Scopes.AsTracking() where scope.Name == name select scope).FirstOrDefaultAsync(cancellationToken); } @@ -241,7 +241,7 @@ namespace OpenIddict.EntityFrameworkCore } return ImmutableArray.CreateRange( - await (from scope in Scopes + await (from scope in Scopes.AsTracking() where names.Contains(scope.Name) select scope).ToListAsync(cancellationToken)); } @@ -268,7 +268,7 @@ namespace OpenIddict.EntityFrameworkCore // are retrieved, a second pass is made to ensure only valid elements are returned. // Implementers that use this method in a hot path may want to override this method // to use SQL Server 2016 functions like JSON_VALUE to make the query more efficient. - var scopes = await (from scope in Scopes + var scopes = await (from scope in Scopes.AsTracking() where scope.Resources.Contains(resource) select scope).ToListAsync(cancellationToken); @@ -488,7 +488,7 @@ namespace OpenIddict.EntityFrameworkCore public virtual async Task> ListAsync( [CanBeNull] int? count, [CanBeNull] int? offset, CancellationToken cancellationToken) { - var query = Scopes.OrderBy(scope => scope.Id).AsQueryable(); + var query = Scopes.OrderBy(scope => scope.Id).AsTracking(); if (offset.HasValue) { diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index 80d5dfc6..88f70e71 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -201,6 +201,139 @@ namespace OpenIddict.EntityFrameworkCore } } + /// + /// 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)); + } + + IQueryable Query(IQueryable applications, IQueryable tokens, TKey key, string principal) + => from token in tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() + where token.Subject == subject + join application in applications.AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(key) + select token; + + return ImmutableArray.CreateRange(await Query(Applications, Tokens, + ConvertIdentifierFromString(client), 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 identifier cannot be null or empty.", nameof(client)); + } + + if (string.IsNullOrEmpty(status)) + { + throw new ArgumentException("The status cannot be null or empty.", nameof(status)); + } + + IQueryable Query(IQueryable applications, + IQueryable tokens, TKey key, string principal, string state) + => from token in tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() + where token.Subject == subject && + token.Status == status + join application in applications.AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(key) + select token; + + return ImmutableArray.CreateRange(await Query(Applications, Tokens, + ConvertIdentifierFromString(client), subject, 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)); + } + + // 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. + + IQueryable Query(IQueryable applications, + IQueryable tokens, TKey key, string principal, string state, string kind) + => from token in tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() + where token.Subject == subject && + token.Status == status && + token.Type == type + join application in applications.AsTracking() on token.Application.Id equals application.Id + where application.Id.Equals(key) + select token; + + return ImmutableArray.CreateRange(await Query(Applications, Tokens, + ConvertIdentifierFromString(client), subject, status, type).ToListAsync(cancellationToken)); + } + /// /// Retrieves the list of tokens corresponding to the specified application identifier. /// @@ -281,7 +414,7 @@ namespace OpenIddict.EntityFrameworkCore var key = ConvertIdentifierFromString(identifier); - return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() where token.Id.Equals(key) select token).FirstOrDefaultAsync(cancellationToken); } @@ -303,7 +436,7 @@ namespace OpenIddict.EntityFrameworkCore throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + return (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() where token.ReferenceId == identifier select token).FirstOrDefaultAsync(cancellationToken); } @@ -325,7 +458,7 @@ namespace OpenIddict.EntityFrameworkCore } return ImmutableArray.CreateRange( - await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization) + await (from token in Tokens.Include(token => token.Application).Include(token => token.Authorization).AsTracking() where token.Subject == subject select token).ToListAsync(cancellationToken)); } @@ -354,7 +487,7 @@ namespace OpenIddict.EntityFrameworkCore async Task RetrieveApplicationIdAsync() { IQueryable Query(IQueryable tokens, TKey key) - => from element in tokens + => from element in tokens.AsTracking() where element.Id.Equals(key) && element.Application != null select element.Application.Id; @@ -416,7 +549,7 @@ namespace OpenIddict.EntityFrameworkCore async Task RetrieveAuthorizationIdAsync() { IQueryable Query(IQueryable tokens, TKey key) - => from element in tokens + => from element in tokens.AsTracking() where element.Id.Equals(key) && element.Authorization != null select element.Authorization.Id; @@ -649,7 +782,7 @@ namespace OpenIddict.EntityFrameworkCore var query = Tokens.Include(token => token.Application) .Include(token => token.Authorization) .OrderBy(token => token.Id) - .AsQueryable(); + .AsTracking(); if (offset.HasValue) { diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbContext.cs index 89cc4069..04f9b1a5 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 e706e6df..55b7dc75 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.Value.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 82629066..b0861a8e 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.Value.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.Value.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.Value.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 afb2de66..d29ef561 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -184,7 +184,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.HttpContext, context.Request, context.Ticket.Properties); - foreach (var parameter in parameters.ToArray()) + foreach (var parameter in parameters.ToList()) { context.Response.AddParameter(parameter.Item2, parameter.Item3); context.Ticket.RemoveProperty(parameter.Item1); diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbContextTests.cs index 6ab3e09c..67d59621 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>(); @@ -83,8 +84,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); } @@ -182,6 +183,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()); } @@ -206,6 +208,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()); } @@ -248,6 +251,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()); } @@ -258,6 +262,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>()); @@ -269,6 +277,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()))