From 55f358689c640257209bc62935dfea66db113f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Fri, 12 Oct 2018 13:35:17 +0200 Subject: [PATCH] Introduce built-in entity caching support in the managers and allow disabling the additional checks --- samples/Mvc.Server/Startup.cs | 1 - .../Caches/IOpenIddictApplicationCache.cs | 86 +++ .../Caches/IOpenIddictAuthorizationCache.cs | 135 +++++ .../Caches/IOpenIddictScopeCache.cs | 84 +++ .../Caches/IOpenIddictTokenCache.cs | 141 +++++ .../Descriptors/OpenIddictTokenDescriptor.cs | 2 + .../Managers/IOpenIddictTokenManager.cs | 13 +- .../OpenIddictConstants.cs | 3 - .../Stores/IOpenIddictTokenStore.cs | 2 +- .../Caches/OpenIddictApplicationCache.cs | 314 ++++++++++ .../Caches/OpenIddictAuthorizationCache.cs | 511 ++++++++++++++++ .../Caches/OpenIddictScopeCache.cs | 296 +++++++++ .../Caches/OpenIddictTokenCache.cs | 571 ++++++++++++++++++ .../Managers/OpenIddictApplicationManager.cs | 126 +++- .../OpenIddictAuthorizationManager.cs | 186 +++++- .../Managers/OpenIddictScopeManager.cs | 98 ++- .../Managers/OpenIddictTokenManager.cs | 303 +++++++--- src/OpenIddict.Core/OpenIddict.Core.csproj | 1 + src/OpenIddict.Core/OpenIddictCoreBuilder.cs | 111 ++-- .../OpenIddictCoreExtensions.cs | 5 + src/OpenIddict.Core/OpenIddictCoreOptions.cs | 25 + .../OpenIddictApplicationStoreResolver.cs | 4 +- .../OpenIddictAuthorizationStoreResolver.cs | 4 +- .../Resolvers/OpenIddictScopeStoreResolver.cs | 4 +- .../Resolvers/OpenIddictTokenStoreResolver.cs | 4 +- .../OpenIddictEntityFrameworkExtensions.cs | 5 + .../Stores/OpenIddictApplicationStore.cs | 25 +- .../Stores/OpenIddictAuthorizationStore.cs | 30 +- .../Stores/OpenIddictScopeStore.cs | 15 +- .../Stores/OpenIddictTokenStore.cs | 17 +- ...OpenIddictEntityFrameworkCoreExtensions.cs | 5 + .../Stores/OpenIddictApplicationStore.cs | 32 +- .../Stores/OpenIddictAuthorizationStore.cs | 30 +- .../Stores/OpenIddictScopeStore.cs | 15 +- .../Stores/OpenIddictTokenStore.cs | 17 +- .../OpenIddictMongoDbExtensions.cs | 4 +- .../OpenIddictApplicationStoreResolver.cs | 4 +- .../OpenIddictAuthorizationStoreResolver.cs | 4 +- .../Resolvers/OpenIddictScopeStoreResolver.cs | 4 +- .../Resolvers/OpenIddictTokenStoreResolver.cs | 4 +- .../Stores/OpenIddictApplicationStore.cs | 29 +- .../Stores/OpenIddictAuthorizationStore.cs | 20 +- .../Stores/OpenIddictScopeStore.cs | 14 +- .../Stores/OpenIddictTokenStore.cs | 22 +- ...OpenIddictServerProvider.Authentication.cs | 4 - .../OpenIddictServerProvider.Exchange.cs | 16 +- .../OpenIddictServerProvider.Helpers.cs | 86 ++- .../OpenIddictServerProvider.Introspection.cs | 15 +- .../OpenIddictServerProvider.Revocation.cs | 11 +- .../Internal/OpenIddictServerProvider.cs | 14 +- .../Internal/OpenIddictValidationProvider.cs | 52 +- .../OpenIddictCoreBuilderTests.cs | 92 ++- .../OpenIddictMongoDbExtensionsTests.cs | 14 - ...OpenIddictApplicationStoreResolverTests.cs | 1 - ...enIddictAuthorizationStoreResolverTests.cs | 1 - .../OpenIddictScopeStoreResolverTests.cs | 1 - .../OpenIddictTokenStoreResolverTests.cs | 1 - .../OpenIddictServerProviderTests.Exchange.cs | 12 +- ...IddictServerProviderTests.Introspection.cs | 14 +- ...penIddictServerProviderTests.Revocation.cs | 4 +- ...IddictServerProviderTests.Serialization.cs | 366 ++++++++++- .../Internal/OpenIddictServerProviderTests.cs | 18 +- .../OpenIddictValidationProviderTests.cs | 155 ++++- 63 files changed, 3712 insertions(+), 491 deletions(-) create mode 100644 src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs create mode 100644 src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs create mode 100644 src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs create mode 100644 src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs create mode 100644 src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs create mode 100644 src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs create mode 100644 src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs create mode 100644 src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 09ff3811..0447f8ae 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Primitives; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs new file mode 100644 index 00000000..023d9d04 --- /dev/null +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs @@ -0,0 +1,86 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Abstractions +{ + /// + /// Provides methods allowing to cache applications after retrieving them from the store. + /// + /// The type of the Application entity. + public interface IOpenIddictApplicationCache where TApplication : class + { + /// + /// Add the specified application to the cache. + /// + /// The application to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken); + + /// + /// Retrieves an application using its client identifier. + /// + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + ValueTask FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client applications corresponding to the specified redirect_uri. + /// + ValueTask> FindByPostLogoutRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken); + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client applications corresponding to the specified redirect_uri. + /// + ValueTask> FindByRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken); + + /// + /// Removes the specified application from the cache. + /// + /// The application to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs new file mode 100644 index 00000000..779fccd3 --- /dev/null +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Abstractions +{ + /// + /// Provides methods allowing to cache authorizations after retrieving them from the store. + /// + /// The type of the Authorization entity. + public interface IOpenIddictAuthorizationCache where TAuthorization : class + { + /// + /// Add the specified authorization to the cache. + /// + /// The authorization to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task AddAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + + /// + /// Retrieves the authorizations corresponding to the specified + /// subject and associated with the application identifier. + /// + /// The subject associated with the authorization. + /// The client 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 subject/client. + /// + ValueTask> FindAsync( + [NotNull] string subject, [NotNull] string client, 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 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. + /// + ValueTask> FindAsync( + [NotNull] string subject, [NotNull] string client, [NotNull] string status, 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 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. + /// + ValueTask> FindAsync( + [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. + /// + ValueTask> FindAsync( + [NotNull] string subject, [NotNull] string client, [NotNull] string status, + [NotNull] string type, ImmutableArray scopes, CancellationToken cancellationToken); + + /// + /// Retrieves the list of authorizations corresponding to the specified application identifier. + /// + /// The application identifier associated with the authorizations. + /// 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 specified application. + /// + ValueTask> FindByApplicationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// The unique identifier 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 authorization corresponding to the identifier. + /// + ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves all the authorizations corresponding to the specified subject. + /// + /// The subject 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 specified subject. + /// + ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken); + + /// + /// Removes the specified authorization from the cache. + /// + /// The authorization to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs new file mode 100644 index 00000000..44dc3cfe --- /dev/null +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Abstractions +{ + /// + /// Provides methods allowing to cache scopes after retrieving them from the store. + /// + /// The type of the Scope entity. + public interface IOpenIddictScopeCache where TScope : class + { + /// + /// Add the specified scope to the cache. + /// + /// The scope to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task AddAsync([NotNull] TScope scope, CancellationToken cancellationToken); + + /// + /// Retrieves a scope using its unique identifier. + /// + /// The unique identifier associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the identifier. + /// + ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves a scope using its name. + /// + /// The name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the specified name. + /// + ValueTask FindByNameAsync([NotNull] string name, CancellationToken cancellationToken); + + /// + /// Retrieves a list of scopes using their name. + /// + /// The names associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes corresponding to the specified names. + /// + ValueTask> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken); + + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + ValueTask> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken); + + /// + /// Removes the specified scope from the cache. + /// + /// The scope to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task RemoveAsync([NotNull] TScope scope, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs new file mode 100644 index 00000000..2b4aef2c --- /dev/null +++ b/src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Abstractions +{ + /// + /// Provides methods allowing to cache tokens after retrieving them from the store. + /// + /// The type of the Token entity. + public interface IOpenIddictTokenCache where TToken : class + { + /// + /// Add the specified token to the cache. + /// + /// The token to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task AddAsync([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. + /// + ValueTask> 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. + /// + ValueTask> 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. + /// + ValueTask> 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. + /// + /// The application identifier associated with the tokens. + /// 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 specified application. + /// + ValueTask> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of tokens corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the tokens. + /// 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 specified authorization. + /// + ValueTask> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves a token using its unique identifier. + /// + /// The unique identifier 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 token corresponding to the unique identifier. + /// + ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of tokens corresponding to the specified reference identifier. + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// + /// The reference identifier associated with the tokens. + /// 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 specified reference identifier. + /// + ValueTask FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// 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 specified subject. + /// + ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken); + + /// + /// Removes the specified token from the cache. + /// + /// The token to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs index e9b059df..7ff16ec2 100644 --- a/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs +++ b/src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs @@ -49,6 +49,8 @@ namespace OpenIddict.Abstractions /// /// Gets or sets the reference identifier associated with the token. + /// Note: depending on the application manager used when creating it, + /// this property may be hashed or encrypted for security reasons. /// public string ReferenceId { get; set; } diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 5dbf934c..20ecd1d7 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -318,7 +318,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - ValueTask GetTokenTypeAsync([NotNull] object token, CancellationToken cancellationToken = default); + ValueTask GetTypeAsync([NotNull] object token, CancellationToken cancellationToken = default); /// /// Determines whether a given token has already been redemeed. @@ -382,17 +382,6 @@ namespace OpenIddict.Abstractions /// Task> ListAsync([NotNull] Func, TState, IQueryable> query, [CanBeNull] TState state, CancellationToken cancellationToken = default); - /// - /// Obfuscates the specified reference identifier so it can be safely stored in a database. - /// By default, this method returns a simple hashed representation computed using SHA256. - /// - /// The client identifier. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - Task ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default); - /// /// Populates the specified descriptor using the properties exposed by the token. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 6337b175..9b73b2ba 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/src/OpenIddict.Abstractions/OpenIddictConstants.cs @@ -272,15 +272,12 @@ namespace OpenIddict.Abstractions public static class Properties { - public const string Application = ".application"; public const string AuthenticationTicket = ".authentication_ticket"; public const string Error = ".error"; public const string ErrorDescription = ".error_description"; public const string ErrorUri = ".error_uri"; public const string InternalAuthorizationId = ".internal_authorization_id"; public const string InternalTokenId = ".internal_token_id"; - public const string ReferenceToken = ".reference_token"; - public const string Token = ".token"; } public static class PropertyTypes diff --git a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs index 1bdbf7c0..722bbbe6 100644 --- a/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs +++ b/src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs @@ -297,7 +297,7 @@ namespace OpenIddict.Abstractions /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken); + ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken); /// /// Instantiates a new token. diff --git a/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs new file mode 100644 index 00000000..8b11df1e --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs @@ -0,0 +1,314 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; + +namespace OpenIddict.Core +{ + /// + /// Provides methods allowing to cache applications after retrieving them from the store. + /// + /// The type of the Application entity. + public class OpenIddictApplicationCache : IOpenIddictApplicationCache, IDisposable where TApplication : class + { + private readonly IMemoryCache _cache; + private readonly IOpenIddictApplicationStore _store; + private readonly IOptionsMonitor _options; + + public OpenIddictApplicationCache( + [NotNull] IOptionsMonitor options, + [NotNull] IOpenIddictApplicationStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.CurrentValue.EntityCacheLimit + }); + + _options = options; + _store = resolver.Get(); + } + + /// + /// Add the specified application to the cache. + /// + /// The application to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(application, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(application); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByClientIdAsync), + Identifier = await _store.GetClientIdAsync(application, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(application); + } + } + + /// + /// Disposes the cache held by this instance. + /// + public void Dispose() => _cache.Dispose(); + + /// + /// Retrieves an application using its client identifier. + /// + /// The client identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public ValueTask FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByClientIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TApplication application)) + { + return new ValueTask(application); + } + + async Task ExecuteAsync() + { + if ((application = await _store.FindByClientIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(application, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(application); + } + + return application; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client application corresponding to the identifier. + /// + public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TApplication application)) + { + return new ValueTask(application); + } + + async Task ExecuteAsync() + { + if ((application = await _store.FindByIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(application, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(application); + } + + return application; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client applications corresponding to the specified post_logout_redirect_uri. + /// + public ValueTask> FindByPostLogoutRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + var parameters = new + { + Method = nameof(FindByPostLogoutRedirectUriAsync), + Address = address + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray applications)) + { + return new ValueTask>(applications); + } + + async Task> ExecuteAsync() + { + foreach (var application in (applications = await _store.FindByPostLogoutRedirectUriAsync(address, cancellationToken))) + { + await AddAsync(application, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(applications.Length); + entry.SetValue(applications); + } + + return applications; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client applications corresponding to the specified redirect_uri. + /// + public ValueTask> FindByRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(address)) + { + throw new ArgumentException("The address cannot be null or empty.", nameof(address)); + } + + var parameters = new + { + Method = nameof(FindByRedirectUriAsync), + Address = address + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray applications)) + { + return new ValueTask>(applications); + } + + async Task> ExecuteAsync() + { + foreach (var application in (applications = await _store.FindByRedirectUriAsync(address, cancellationToken))) + { + await AddAsync(application, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(applications.Length); + entry.SetValue(applications); + } + + return applications; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Removes the specified application from the cache. + /// + /// The application to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken) + { + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + _cache.Remove(new + { + Method = nameof(FindByClientIdAsync), + Identifier = await _store.GetClientIdAsync(application, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(application, cancellationToken) + }); + + foreach (var address in await _store.GetPostLogoutRedirectUrisAsync(application, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByPostLogoutRedirectUriAsync), + Address = address + }); + } + + foreach (var address in await _store.GetRedirectUrisAsync(application, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByRedirectUriAsync), + Address = address + }); + } + } + } +} diff --git a/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs new file mode 100644 index 00000000..8e51c15e --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs @@ -0,0 +1,511 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; + +namespace OpenIddict.Core +{ + /// + /// Provides methods allowing to cache authorizations after retrieving them from the store. + /// + /// The type of the Authorization entity. + public class OpenIddictAuthorizationCache : IOpenIddictAuthorizationCache, IDisposable where TAuthorization : class + { + private readonly IMemoryCache _cache; + private readonly IOpenIddictAuthorizationStore _store; + private readonly IOptionsMonitor _options; + + public OpenIddictAuthorizationCache( + [NotNull] IOptionsMonitor options, + [NotNull] IOpenIddictAuthorizationStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.CurrentValue.EntityCacheLimit + }); + + _options = options; + _store = resolver.Get(); + } + + /// + /// Add the specified authorization to the cache. + /// + /// The authorization to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task AddAsync(TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(authorization, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(authorization); + } + } + + /// + /// Disposes the cache held by this instance. + /// + public void Dispose() => _cache.Dispose(); + + /// + /// Retrieves the authorizations corresponding to the specified + /// subject and associated with the application identifier. + /// + /// The subject associated with the authorization. + /// The client 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 subject/client. + /// + public ValueTask> 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 identifier cannot be null or empty.", nameof(client)); + } + + var parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray authorizations)) + { + return new ValueTask>(authorizations); + } + + async Task> ExecuteAsync() + { + foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, cancellationToken))) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(authorizations.Length); + entry.SetValue(authorizations); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves the authorizations matching the specified parameters. + /// + /// The subject associated with the authorization. + /// The client associated with the authorization. + /// The authorization status. + /// 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 ValueTask> 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)); + } + + var parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client, + Status = status + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray authorizations)) + { + return new ValueTask>(authorizations); + } + + async Task> ExecuteAsync() + { + foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, status, cancellationToken))) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(authorizations.Length); + entry.SetValue(authorizations); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// 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 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 ValueTask> 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 parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client, + Status = status, + Type = type + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray authorizations)) + { + return new ValueTask>(authorizations); + } + + async Task> ExecuteAsync() + { + foreach (var authorization in (authorizations = await _store.FindAsync(subject, client, status, type, cancellationToken))) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(authorizations.Length); + entry.SetValue(authorizations); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// 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 ValueTask> 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)); + } + + // Note: this method is only partially cached. + + async Task> ExecuteAsync() + { + var authorizations = await _store.FindAsync(subject, client, status, type, scopes, cancellationToken); + + foreach (var authorization in authorizations) + { + await AddAsync(authorization, cancellationToken); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves the list of authorizations corresponding to the specified application identifier. + /// + /// The application identifier associated with the authorizations. + /// 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 specified application. + /// + public ValueTask> FindByApplicationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray authorizations)) + { + return new ValueTask>(authorizations); + } + + async Task> ExecuteAsync() + { + foreach (var authorization in (authorizations = await _store.FindByApplicationIdAsync(identifier, cancellationToken))) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(authorizations.Length); + entry.SetValue(authorizations); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves an authorization using its unique identifier. + /// + /// The unique identifier 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 authorization corresponding to the identifier. + /// + public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TAuthorization authorization)) + { + return new ValueTask(authorization); + } + + async Task ExecuteAsync() + { + if ((authorization = await _store.FindByIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(authorization); + } + + return authorization; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves all the authorizations corresponding to the specified subject. + /// + /// The subject 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 specified subject. + /// + public ValueTask> FindBySubjectAsync( + [NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + var parameters = new + { + Method = nameof(FindBySubjectAsync), + Subject = subject + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray authorizations)) + { + return new ValueTask>(authorizations); + } + + async Task> ExecuteAsync() + { + foreach (var authorization in (authorizations = await _store.FindBySubjectAsync(subject, cancellationToken))) + { + await AddAsync(authorization, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(authorizations.Length); + entry.SetValue(authorizations); + } + + return authorizations; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Removes the specified authorization from the cache. + /// + /// The authorization to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + { + if (authorization == null) + { + throw new ArgumentNullException(nameof(authorization)); + } + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), + Status = await _store.GetStatusAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken), + Client = await _store.GetApplicationIdAsync(authorization, cancellationToken), + Status = await _store.GetStatusAsync(authorization, cancellationToken), + Type = await _store.GetTypeAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = await _store.GetApplicationIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(authorization, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindBySubjectAsync), + Subject = await _store.GetSubjectAsync(authorization, cancellationToken) + }); + } + } +} diff --git a/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs new file mode 100644 index 00000000..4bac92a8 --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs @@ -0,0 +1,296 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; + +namespace OpenIddict.Core +{ + /// + /// Provides methods allowing to cache scopes after retrieving them from the store. + /// + /// The type of the Scope entity. + public class OpenIddictScopeCache : IOpenIddictScopeCache, IDisposable where TScope : class + { + private readonly IMemoryCache _cache; + private readonly IOpenIddictScopeStore _store; + private readonly IOptionsMonitor _options; + + public OpenIddictScopeCache( + [NotNull] IOptionsMonitor options, + [NotNull] IOpenIddictScopeStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.CurrentValue.EntityCacheLimit + }); + + _options = options; + _store = resolver.Get(); + } + + /// + /// Add the specified scope to the cache. + /// + /// The scope to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task AddAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(scope, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(scope); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByNameAsync), + Name = await _store.GetNameAsync(scope, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(scope); + } + } + + /// + /// Disposes the cache held by this instance. + /// + public void Dispose() => _cache.Dispose(); + + /// + /// Retrieves a scope using its unique identifier. + /// + /// The unique identifier associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the identifier. + /// + public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TScope scope)) + { + return new ValueTask(scope); + } + + async Task ExecuteAsync() + { + if ((scope = await _store.FindByIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(scope, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(scope); + } + + return scope; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves a scope using its name. + /// + /// The name associated with the scope. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scope corresponding to the specified name. + /// + public ValueTask FindByNameAsync([NotNull] string name, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); + } + + var parameters = new + { + Method = nameof(FindByNameAsync), + Name = name + }; + + if (_cache.TryGetValue(parameters, out TScope scope)) + { + return new ValueTask(scope); + } + + async Task ExecuteAsync() + { + if ((scope = await _store.FindByNameAsync(name, cancellationToken)) != null) + { + await AddAsync(scope, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(scope); + } + + return scope; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves a list of scopes using their name. + /// + /// The names associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes corresponding to the specified names. + /// + public ValueTask> FindByNamesAsync(ImmutableArray names, CancellationToken cancellationToken) + { + if (names.IsDefaultOrEmpty) + { + return new ValueTask>(ImmutableArray.Create()); + } + + if (names.Any(name => string.IsNullOrEmpty(name))) + { + throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); + } + + // Note: this method is only partially cached. + + async Task> ExecuteAsync() + { + var scopes = await _store.FindByNamesAsync(names, cancellationToken); + + foreach (var scope in scopes) + { + await AddAsync(scope, cancellationToken); + } + + return scopes; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves all the scopes that contain the specified resource. + /// + /// The resource associated with the scopes. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the scopes associated with the specified resource. + /// + public ValueTask> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(resource)) + { + throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); + } + + var parameters = new + { + Method = nameof(FindByResourceAsync), + Resource = resource + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray scopes)) + { + return new ValueTask>(scopes); + } + + async Task> ExecuteAsync() + { + foreach (var scope in (scopes = await _store.FindByResourceAsync(resource, cancellationToken))) + { + await AddAsync(scope, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(scopes.Length); + entry.SetValue(scopes); + } + + return scopes; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Removes the specified scope from the cache. + /// + /// The scope to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task RemoveAsync([NotNull] TScope scope, CancellationToken cancellationToken) + { + if (scope == null) + { + throw new ArgumentNullException(nameof(scope)); + } + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(scope, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByNameAsync), + Name = await _store.GetNameAsync(scope, cancellationToken) + }); + + foreach (var resource in await _store.GetResourcesAsync(scope, cancellationToken)) + { + _cache.Remove(new + { + Method = nameof(FindByResourceAsync), + Resource = resource + }); + } + } + } +} diff --git a/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs new file mode 100644 index 00000000..37873e1a --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs @@ -0,0 +1,571 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using OpenIddict.Abstractions; + +namespace OpenIddict.Core +{ + /// + /// Provides methods allowing to cache tokens after retrieving them from the store. + /// + /// The type of the Token entity. + public class OpenIddictTokenCache : IOpenIddictTokenCache, IDisposable where TToken : class + { + private readonly IMemoryCache _cache; + private readonly IOpenIddictTokenStore _store; + private readonly IOptionsMonitor _options; + + public OpenIddictTokenCache( + [NotNull] IOptionsMonitor options, + [NotNull] IOpenIddictTokenStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.CurrentValue.EntityCacheLimit + }); + + _options = options; + _store = resolver.Get(); + } + + /// + /// Add the specified token to the cache. + /// + /// The token to add to the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task AddAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(token, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(token); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByReferenceIdAsync), + Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) + })) + { + entry.SetSize(1L); + entry.SetValue(token); + } + } + + /// + /// Disposes the cache held by this instance. + /// + public void Dispose() => _cache.Dispose(); + + /// + /// 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 ValueTask> 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 identifier cannot be null or empty.", nameof(client)); + } + + var parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindAsync(subject, client, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// 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 ValueTask> 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)); + } + + var parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client, + Status = status + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindAsync(subject, client, status, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// 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 ValueTask> 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 parameters = new + { + Method = nameof(FindAsync), + Subject = subject, + Client = client, + Status = status, + Type = type + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindAsync(subject, client, status, type, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves the list of tokens corresponding to the specified application identifier. + /// + /// The application identifier associated with the tokens. + /// 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 specified application. + /// + public ValueTask> FindByApplicationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindByApplicationIdAsync(identifier, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves the list of tokens corresponding to the specified authorization identifier. + /// + /// The authorization identifier associated with the tokens. + /// 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 specified authorization. + /// + public ValueTask> FindByAuthorizationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByAuthorizationIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindByAuthorizationIdAsync(identifier, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Retrieves a token using its unique identifier. + /// + /// The unique identifier 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 token corresponding to the unique identifier. + /// + public ValueTask FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TToken token)) + { + return new ValueTask(token); + } + + async Task ExecuteAsync() + { + if ((token = await _store.FindByIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(token); + } + + return token; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves the list of tokens corresponding to the specified reference identifier. + /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// + /// The reference identifier associated with the tokens. + /// 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 specified reference identifier. + /// + public ValueTask FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + var parameters = new + { + Method = nameof(FindByReferenceIdAsync), + Identifier = identifier + }; + + if (_cache.TryGetValue(parameters, out TToken token)) + { + return new ValueTask(token); + } + + async Task ExecuteAsync() + { + if ((token = await _store.FindByReferenceIdAsync(identifier, cancellationToken)) != null) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(1L); + entry.SetValue(token); + } + + return token; + } + + return new ValueTask(ExecuteAsync()); + } + + /// + /// Retrieves the list of tokens corresponding to the specified subject. + /// + /// The subject associated with the tokens. + /// 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 specified subject. + /// + public ValueTask> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(subject)) + { + throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); + } + + var parameters = new + { + Method = nameof(FindBySubjectAsync), + Identifier = subject + }; + + if (_cache.TryGetValue(parameters, out ImmutableArray tokens)) + { + return new ValueTask>(tokens); + } + + async Task> ExecuteAsync() + { + foreach (var token in (tokens = await _store.FindBySubjectAsync(subject, cancellationToken))) + { + await AddAsync(token, cancellationToken); + } + + using (var entry = _cache.CreateEntry(parameters)) + { + entry.SetSize(tokens.Length); + entry.SetValue(tokens); + } + + return tokens; + } + + return new ValueTask>(ExecuteAsync()); + } + + /// + /// Removes the specified token from the cache. + /// + /// The token to remove from the cache. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken) + { + if (token == null) + { + throw new ArgumentNullException(nameof(token)); + } + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken), + Status = await _store.GetStatusAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken), + Client = await _store.GetApplicationIdAsync(token, cancellationToken), + Status = await _store.GetStatusAsync(token, cancellationToken), + Type = await _store.GetTypeAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByApplicationIdAsync), + Identifier = await _store.GetApplicationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByAuthorizationIdAsync), + Identifier = await _store.GetAuthorizationIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindByReferenceIdAsync), + Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) + }); + + _cache.Remove(new + { + Method = nameof(FindBySubjectAsync), + Subject = await _store.GetSubjectAsync(token, cancellationToken) + }); + } + } +} diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 8aaef7b1..48dd73e9 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -26,15 +26,22 @@ namespace OpenIddict.Core public class OpenIddictApplicationManager : IOpenIddictApplicationManager where TApplication : class { public OpenIddictApplicationManager( + [NotNull] IOpenIddictApplicationCache cache, [NotNull] IOpenIddictApplicationStoreResolver resolver, [NotNull] ILogger> logger, [NotNull] IOptionsMonitor options) { + Cache = cache; Store = resolver.Get(); Logger = logger; Options = options; } + /// + /// Gets the cache associated with the current manager. + /// + protected IOpenIddictApplicationCache Cache { get; } + /// /// Gets the logger associated with the current manager. /// @@ -115,7 +122,7 @@ namespace OpenIddict.Core if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken))) { - throw new ArgumentException("The client secret hash cannot be directly set on the application entity."); + throw new ArgumentException("The client secret hash cannot be set on the application entity.", nameof(application)); } // If no client type was specified, assume it's a public application if no secret was provided. @@ -174,7 +181,7 @@ namespace OpenIddict.Core var application = await Store.InstantiateAsync(cancellationToken); if (application == null) { - throw new InvalidOperationException("An error occurred while trying to create a new application"); + throw new InvalidOperationException("An error occurred while trying to create a new application."); } await PopulateAsync(application, descriptor, cancellationToken); @@ -201,58 +208,89 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { throw new ArgumentNullException(nameof(application)); } - return Store.DeleteAsync(application, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(application, cancellationToken); + } + + await Store.DeleteAsync(application, cancellationToken); } /// - /// Retrieves an application using its unique identifier. + /// Retrieves an application using its client identifier. /// - /// The unique identifier associated with the application. + /// The client identifier associated with the application. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByClientIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByIdAsync(identifier, cancellationToken); + var application = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByClientIdAsync(identifier, cancellationToken) : + await Cache.FindByClientIdAsync(identifier, cancellationToken); + + if (application == null) + { + return null; + } + + // 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. + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal)) + { + return null; + } + + return application; } /// - /// Retrieves an application using its client identifier. + /// Retrieves an application using its unique identifier. /// - /// The client identifier associated with the application. + /// The unique identifier associated with the application. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, /// whose result returns the client application corresponding to the identifier. /// - public virtual async Task FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } + var application = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByIdAsync(identifier, cancellationToken) : + await Cache.FindByIdAsync(identifier, cancellationToken); + + if (application == null) + { + return null; + } + // 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 application = await Store.FindByClientIdAsync(identifier, cancellationToken); - if (application == null || - !string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal)) + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal)) { return null; } @@ -277,16 +315,24 @@ namespace OpenIddict.Core throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - // 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 applications = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken) : + await Cache.FindByPostLogoutRedirectUriAsync(address, cancellationToken); - var applications = await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken); if (applications.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return applications; + } + + // 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 builder = ImmutableArray.CreateBuilder(applications.Length); foreach (var application in applications) @@ -323,16 +369,24 @@ namespace OpenIddict.Core throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - // 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 applications = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByRedirectUriAsync(address, cancellationToken) : + await Cache.FindByRedirectUriAsync(address, cancellationToken); - var applications = await Store.FindByRedirectUriAsync(address, cancellationToken); if (applications.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return applications; + } + + // 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 builder = ImmutableArray.CreateBuilder(applications.Length); foreach (var application in applications) @@ -365,6 +419,11 @@ namespace OpenIddict.Core public virtual Task GetAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return GetAsync((applications, state) => state(applications), query, cancellationToken); } @@ -401,7 +460,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the client identifier associated with the application. /// - public virtual ValueTask GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) + public virtual ValueTask GetClientIdAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { @@ -440,7 +500,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the consent type of the application (by default, "explicit"). /// - public virtual async ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) + public virtual async ValueTask GetConsentTypeAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { @@ -465,7 +526,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the display name associated with the application. /// - public virtual ValueTask GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) + public virtual ValueTask GetDisplayNameAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { @@ -673,6 +735,11 @@ namespace OpenIddict.Core public virtual Task> ListAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return ListAsync((applications, state) => state(applications), query, cancellationToken); } @@ -832,6 +899,11 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(application, cancellationToken); + } + await Store.UpdateAsync(application, cancellationToken); } @@ -894,7 +966,7 @@ namespace OpenIddict.Core var comparand = await Store.GetClientSecretAsync(application, cancellationToken); await PopulateAsync(application, descriptor, cancellationToken); - // If the client secret was updated, re-obfuscate it before persisting the changes. + // If the client secret was updated, use the overload accepting a secret parameter. var secret = await Store.GetClientSecretAsync(application, cancellationToken); if (!string.Equals(secret, comparand, StringComparison.Ordinal)) { diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 4c23cc53..beca6591 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -26,15 +26,22 @@ namespace OpenIddict.Core public class OpenIddictAuthorizationManager : IOpenIddictAuthorizationManager where TAuthorization : class { public OpenIddictAuthorizationManager( + [NotNull] IOpenIddictAuthorizationCache cache, [NotNull] IOpenIddictAuthorizationStoreResolver resolver, [NotNull] ILogger> logger, [NotNull] IOptionsMonitor options) { + Cache = cache; Store = resolver.Get(); Logger = logger; Options = options; } + /// + /// Gets the cache associated with the current manager. + /// + protected IOpenIddictAuthorizationCache Cache { get; } + /// /// Gets the logger associated with the current manager. /// @@ -217,14 +224,19 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { throw new ArgumentNullException(nameof(authorization)); } - return Store.DeleteAsync(authorization, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(authorization, cancellationToken); + } + + await Store.DeleteAsync(authorization, cancellationToken); } /// @@ -251,14 +263,22 @@ namespace OpenIddict.Core throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); } + var authorizations = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, cancellationToken) : + await Cache.FindAsync(subject, client, cancellationToken); + + if (authorizations.IsEmpty) + { + return ImmutableArray.Create(); + } + // 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, cancellationToken); - if (authorizations.IsEmpty) + if (Options.CurrentValue.DisableAdditionalFiltering) { - return ImmutableArray.Create(); + return authorizations; } var builder = ImmutableArray.CreateBuilder(authorizations.Length); @@ -306,16 +326,24 @@ namespace OpenIddict.Core 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 authorizations = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, status, cancellationToken) : + await Cache.FindAsync(subject, client, status, cancellationToken); - var authorizations = await Store.FindAsync(subject, client, status, cancellationToken); if (authorizations.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return authorizations; + } + + // 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 builder = ImmutableArray.CreateBuilder(authorizations.Length); foreach (var authorization in authorizations) @@ -367,16 +395,24 @@ namespace OpenIddict.Core 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, status, type, cancellationToken) : + await Cache.FindAsync(subject, client, status, type, cancellationToken); - var authorizations = await Store.FindAsync(subject, client, status, type, cancellationToken); if (authorizations.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return authorizations; + } + + // 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 builder = ImmutableArray.CreateBuilder(authorizations.Length); foreach (var authorization in authorizations) @@ -430,16 +466,24 @@ namespace OpenIddict.Core 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, status, type, scopes, cancellationToken) : + await Cache.FindAsync(subject, client, status, type, scopes, cancellationToken); - var authorizations = await Store.FindAsync(subject, client, status, type, scopes, cancellationToken); if (authorizations.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return authorizations; + } + + // 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 builder = ImmutableArray.CreateBuilder(authorizations.Length); foreach (var authorization in authorizations) @@ -465,7 +509,7 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorizations corresponding to the specified application. /// - public virtual Task> FindByApplicationIdAsync( + public virtual async Task> FindByApplicationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) @@ -473,7 +517,37 @@ namespace OpenIddict.Core throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByApplicationIdAsync(identifier, cancellationToken); + var authorizations = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByApplicationIdAsync(identifier, cancellationToken) : + await Cache.FindByApplicationIdAsync(identifier, cancellationToken); + + if (authorizations.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return authorizations; + } + + // 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 builder = ImmutableArray.CreateBuilder(authorizations.Length); + + foreach (var authorization in authorizations) + { + if (string.Equals(await Store.GetApplicationIdAsync(authorization, cancellationToken), identifier, StringComparison.Ordinal)) + { + builder.Add(authorization); + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); } /// @@ -485,14 +559,32 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the authorization corresponding to the identifier. /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByIdAsync(identifier, cancellationToken); + var authorization = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByIdAsync(identifier, cancellationToken) : + await Cache.FindByIdAsync(identifier, cancellationToken); + + if (authorization == null) + { + return null; + } + + // 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. + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(authorization, cancellationToken), identifier, StringComparison.Ordinal)) + { + return null; + } + + return authorization; } /// @@ -512,16 +604,24 @@ namespace OpenIddict.Core throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - // 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindBySubjectAsync(subject, cancellationToken) : + await Cache.FindBySubjectAsync(subject, cancellationToken); - var authorizations = await Store.FindBySubjectAsync(subject, cancellationToken); if (authorizations.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return authorizations; + } + + // 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 builder = ImmutableArray.CreateBuilder(authorizations.Length); foreach (var authorization in authorizations) @@ -570,6 +670,11 @@ namespace OpenIddict.Core public virtual Task GetAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return GetAsync((authorizations, state) => state(authorizations), query, cancellationToken); } @@ -625,7 +730,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the scopes associated with the specified authorization. /// - public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual ValueTask> GetScopesAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -644,7 +750,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the status associated with the specified authorization. /// - public virtual ValueTask GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual ValueTask GetStatusAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -663,7 +770,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the subject associated with the specified authorization. /// - public virtual ValueTask GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual ValueTask GetSubjectAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -682,7 +790,8 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the type associated with the specified authorization. /// - public virtual ValueTask GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual ValueTask GetTypeAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -740,7 +849,8 @@ namespace OpenIddict.Core /// The authorization. /// The that can be used to abort the operation. /// true if the authorization is permanent, false otherwise. - public async Task IsPermanentAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public async Task IsPermanentAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -762,7 +872,8 @@ namespace OpenIddict.Core /// The authorization. /// The that can be used to abort the operation. /// true if the authorization has been revoked, false otherwise. - public virtual async Task IsRevokedAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual async Task IsRevokedAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -784,7 +895,8 @@ namespace OpenIddict.Core /// The authorization. /// The that can be used to abort the operation. /// true if the authorization is valid, false otherwise. - public virtual async Task IsValidAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + public virtual async Task IsValidAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) { if (authorization == null) { @@ -829,6 +941,11 @@ namespace OpenIddict.Core public virtual Task> ListAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return ListAsync((authorizations, state) => state(authorizations), query, cancellationToken); } @@ -998,6 +1115,11 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(authorization, cancellationToken); + } + await Store.UpdateAsync(authorization, cancellationToken); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs index 0dc466ec..4ddf0772 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -26,15 +26,22 @@ namespace OpenIddict.Core public class OpenIddictScopeManager : IOpenIddictScopeManager where TScope : class { public OpenIddictScopeManager( + [NotNull] IOpenIddictScopeCache cache, [NotNull] IOpenIddictScopeStoreResolver resolver, [NotNull] ILogger> logger, [NotNull] IOptionsMonitor options) { + Cache = cache; Store = resolver.Get(); Logger = logger; Options = options; } + /// + /// Gets the cache associated with the current manager. + /// + protected IOpenIddictScopeCache Cache { get; } + /// /// Gets the logger associated with the current manager. /// @@ -151,14 +158,19 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken = default) { if (scope == null) { throw new ArgumentNullException(nameof(scope)); } - return Store.DeleteAsync(scope, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(scope, cancellationToken); + } + + await Store.DeleteAsync(scope, cancellationToken); } /// @@ -170,14 +182,32 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the scope corresponding to the identifier. /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByIdAsync(identifier, cancellationToken); + var scope = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByIdAsync(identifier, cancellationToken) : + await Cache.FindByIdAsync(identifier, cancellationToken); + + if (scope == null) + { + return null; + } + + // 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. + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(scope, cancellationToken), identifier, StringComparison.Ordinal)) + { + return null; + } + + return scope; } /// @@ -196,12 +226,21 @@ namespace OpenIddict.Core throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); } + var scope = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByNameAsync(name, cancellationToken) : + await Cache.FindByNameAsync(name, cancellationToken); + + if (scope == null) + { + return null; + } + // 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 scope = await Store.FindByNameAsync(name, cancellationToken); - if (scope == null || !string.Equals(await Store.GetNameAsync(scope, cancellationToken), name, StringComparison.Ordinal)) + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetNameAsync(scope, cancellationToken), name, StringComparison.Ordinal)) { return null; } @@ -231,16 +270,24 @@ namespace OpenIddict.Core throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); } - // 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 scopes = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByNamesAsync(names, cancellationToken) : + await Cache.FindByNamesAsync(names, cancellationToken); - var scopes = await Store.FindByNamesAsync(names, cancellationToken); if (scopes.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return scopes; + } + + // 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 builder = ImmutableArray.CreateBuilder(scopes.Length); foreach (var scope in scopes) @@ -273,16 +320,24 @@ namespace OpenIddict.Core throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); } - // 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 scopes = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByResourceAsync(resource, cancellationToken) : + await Cache.FindByResourceAsync(resource, cancellationToken); - var scopes = await Store.FindByResourceAsync(resource, cancellationToken); if (scopes.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return scopes; + } + + // 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 builder = ImmutableArray.CreateBuilder(scopes.Length); foreach (var scope in scopes) @@ -314,6 +369,11 @@ namespace OpenIddict.Core public virtual Task GetAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return GetAsync((scopes, state) => state(scopes), query, cancellationToken); } @@ -466,6 +526,11 @@ namespace OpenIddict.Core public virtual Task> ListAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return ListAsync((scopes, state) => state(scopes), query, cancellationToken); } @@ -614,6 +679,11 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(scope, cancellationToken); + } + await Store.UpdateAsync(scope, cancellationToken); } diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 26aa034b..96669932 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -26,15 +26,22 @@ namespace OpenIddict.Core public class OpenIddictTokenManager : IOpenIddictTokenManager where TToken : class { public OpenIddictTokenManager( + [NotNull] IOpenIddictTokenCache cache, [NotNull] IOpenIddictTokenStoreResolver resolver, [NotNull] ILogger> logger, [NotNull] IOptionsMonitor options) { + Cache = cache; Store = resolver.Get(); Logger = logger; Options = options; } + /// + /// Gets the cache associated with the current manager. + /// + protected IOpenIddictTokenCache Cache { get; } + /// /// Gets the logger associated with the current manager. /// @@ -103,6 +110,14 @@ namespace OpenIddict.Core await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Valid, cancellationToken); } + // If a reference identifier was set, obfuscate it. + var identifier = await Store.GetReferenceIdAsync(token, cancellationToken); + if (!string.IsNullOrEmpty(identifier)) + { + identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); + await Store.SetReferenceIdAsync(token, identifier, cancellationToken); + } + var results = await ValidateAsync(token, cancellationToken); if (results.Any(result => result != ValidationResult.Success)) { @@ -157,14 +172,19 @@ namespace OpenIddict.Core /// /// A that can be used to monitor the asynchronous operation. /// - public virtual Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken = default) + public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.DeleteAsync(token, cancellationToken); + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(token, cancellationToken); + } + + await Store.DeleteAsync(token, cancellationToken); } /// @@ -212,16 +232,24 @@ namespace OpenIddict.Core 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, cancellationToken) : + await Cache.FindAsync(subject, client, cancellationToken); - var tokens = await Store.FindAsync(subject, client, cancellationToken); if (tokens.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); foreach (var token in tokens) @@ -267,16 +295,24 @@ namespace OpenIddict.Core 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, status, cancellationToken) : + await Cache.FindAsync(subject, client, status, cancellationToken); - var tokens = await Store.FindAsync(subject, client, status, cancellationToken); if (tokens.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); foreach (var token in tokens) @@ -328,16 +364,24 @@ namespace OpenIddict.Core 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindAsync(subject, client, status, type, cancellationToken) : + await Cache.FindAsync(subject, client, status, type, cancellationToken); - var tokens = await Store.FindAsync(subject, client, status, type, cancellationToken); if (tokens.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); foreach (var token in tokens) @@ -362,7 +406,7 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified application. /// - public virtual Task> FindByApplicationIdAsync( + public virtual async Task> FindByApplicationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) @@ -370,7 +414,37 @@ namespace OpenIddict.Core throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByApplicationIdAsync(identifier, cancellationToken); + var tokens = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByApplicationIdAsync(identifier, cancellationToken) : + await Cache.FindByApplicationIdAsync(identifier, cancellationToken); + + if (tokens.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); + + foreach (var token in tokens) + { + if (string.Equals(await Store.GetApplicationIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal)) + { + builder.Add(token); + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); } /// @@ -382,7 +456,7 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified authorization. /// - public virtual Task> FindByAuthorizationIdAsync( + public virtual async Task> FindByAuthorizationIdAsync( [NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) @@ -390,35 +464,69 @@ namespace OpenIddict.Core throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByAuthorizationIdAsync(identifier, cancellationToken); + var tokens = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByAuthorizationIdAsync(identifier, cancellationToken) : + await Cache.FindByAuthorizationIdAsync(identifier, cancellationToken); + + if (tokens.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); + + foreach (var token in tokens) + { + if (string.Equals(await Store.GetAuthorizationIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal)) + { + builder.Add(token); + } + } + + return builder.Count == builder.Capacity ? + builder.MoveToImmutable() : + builder.ToImmutable(); } /// - /// Retrieves the list of tokens corresponding to the specified reference identifier. - /// Note: the reference identifier may be hashed or encrypted for security reasons. + /// Retrieves a token using its unique identifier. /// - /// The reference identifier associated with the tokens. + /// The unique identifier 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 specified reference identifier. + /// whose result returns the token corresponding to the unique identifier. /// - public virtual async Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } + var token = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByIdAsync(identifier, cancellationToken) : + await Cache.FindByIdAsync(identifier, cancellationToken); + + if (token == null) + { + return null; + } + // 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. - - identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); - - var token = await Store.FindByReferenceIdAsync(identifier, cancellationToken); - if (token == null || - !string.Equals(await Store.GetReferenceIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal)) + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal)) { return null; } @@ -427,22 +535,44 @@ namespace OpenIddict.Core } /// - /// Retrieves a token using its unique identifier. + /// Retrieves the list of tokens corresponding to the specified reference identifier. + /// Note: the reference identifier may be hashed or encrypted for security reasons. /// - /// The unique identifier associated with the token. + /// The reference identifier associated with the tokens. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, - /// whose result returns the token corresponding to the unique identifier. + /// whose result returns the tokens corresponding to the specified reference identifier. /// - public virtual Task FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + public virtual async Task FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(identifier)) { throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); } - return Store.FindByIdAsync(identifier, cancellationToken); + identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); + + var token = Options.CurrentValue.DisableEntityCaching ? + await Store.FindByReferenceIdAsync(identifier, cancellationToken) : + await Cache.FindByReferenceIdAsync(identifier, cancellationToken); + + if (token == null) + { + return null; + } + + // 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. + + if (!Options.CurrentValue.DisableAdditionalFiltering && + !string.Equals(await Store.GetReferenceIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal)) + { + return null; + } + + return token; } /// @@ -462,16 +592,24 @@ namespace OpenIddict.Core throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); } - // 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 = Options.CurrentValue.DisableEntityCaching ? + await Store.FindBySubjectAsync(subject, cancellationToken) : + await Cache.FindBySubjectAsync(subject, cancellationToken); - var tokens = await Store.FindBySubjectAsync(subject, cancellationToken); if (tokens.IsEmpty) { return ImmutableArray.Create(); } + if (Options.CurrentValue.DisableAdditionalFiltering) + { + return tokens; + } + + // 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 builder = ImmutableArray.CreateBuilder(tokens.Length); foreach (var token in tokens) @@ -519,6 +657,11 @@ namespace OpenIddict.Core public virtual Task GetAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return GetAsync((tokens, state) => state(tokens), query, cancellationToken); } @@ -709,14 +852,14 @@ namespace OpenIddict.Core /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) + public virtual ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) { if (token == null) { throw new ArgumentNullException(nameof(token)); } - return Store.GetTokenTypeAsync(token, cancellationToken); + return Store.GetTypeAsync(token, cancellationToken); } /// @@ -814,6 +957,11 @@ namespace OpenIddict.Core public virtual Task> ListAsync( [NotNull] Func, IQueryable> query, CancellationToken cancellationToken = default) { + if (query == null) + { + throw new ArgumentNullException(nameof(query)); + } + return ListAsync((tokens, state) => state(tokens), query, cancellationToken); } @@ -841,30 +989,6 @@ namespace OpenIddict.Core return Store.ListAsync(query, state, cancellationToken); } - /// - /// Obfuscates the specified reference identifier so it can be safely stored in a database. - /// By default, this method returns a simple hashed representation computed using SHA256. - /// - /// The client identifier. - /// The that can be used to abort the operation. - /// - /// A that can be used to monitor the asynchronous operation. - /// - public virtual Task ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(identifier)) - { - throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); - } - - using (var algorithm = SHA256.Create()) - { - // Compute the digest of the generated identifier and use it as the hashed identifier of the reference token. - // Doing that prevents token identifiers stolen from the database from being used as valid reference tokens. - return Task.FromResult(Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(identifier)))); - } - } - /// /// Populates the token using the specified descriptor. /// @@ -929,7 +1053,7 @@ namespace OpenIddict.Core descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken); descriptor.Status = await Store.GetStatusAsync(token, cancellationToken); descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken); - descriptor.Type = await Store.GetTokenTypeAsync(token, cancellationToken); + descriptor.Type = await Store.GetTypeAsync(token, cancellationToken); } /// @@ -1056,6 +1180,11 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + if (!Options.CurrentValue.DisableEntityCaching) + { + await Cache.RemoveAsync(token, cancellationToken); + } + await Store.UpdateAsync(token, cancellationToken); } @@ -1081,7 +1210,18 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(descriptor)); } + // Store the original reference identifier for later comparison. + var comparand = await Store.GetReferenceIdAsync(token, cancellationToken); await PopulateAsync(token, descriptor, cancellationToken); + + // If the reference identifier was updated, re-obfuscate it before persisting the changes. + var identifier = await Store.GetReferenceIdAsync(token, cancellationToken); + if (!string.Equals(identifier, comparand, StringComparison.Ordinal)) + { + identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); + await Store.SetReferenceIdAsync(token, identifier, cancellationToken); + } + await UpdateAsync(token, cancellationToken); } @@ -1122,7 +1262,7 @@ namespace OpenIddict.Core } } - var type = await Store.GetTokenTypeAsync(token, cancellationToken); + var type = await Store.GetTypeAsync(token, cancellationToken); if (string.IsNullOrEmpty(type)) { builder.Add(new ValidationResult("The token type cannot be null or empty.")); @@ -1150,6 +1290,30 @@ namespace OpenIddict.Core builder.ToImmutable(); } + /// + /// Obfuscates the specified reference identifier so it can be safely stored in a database. + /// By default, this method returns a simple hashed representation computed using SHA256. + /// + /// The client identifier. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual Task ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(identifier)) + { + throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); + } + + using (var algorithm = SHA256.Create()) + { + // Compute the digest of the generated identifier and use it as the hashed identifier of the reference token. + // Doing that prevents token identifiers stolen from the database from being used as valid reference tokens. + return Task.FromResult(Convert.ToBase64String(algorithm.ComputeHash(Encoding.UTF8.GetBytes(identifier)))); + } + } + Task IOpenIddictTokenManager.CountAsync(CancellationToken cancellationToken) => CountAsync(cancellationToken); @@ -1225,8 +1389,8 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.GetSubjectAsync(object token, CancellationToken cancellationToken) => GetSubjectAsync((TToken) token, cancellationToken); - ValueTask IOpenIddictTokenManager.GetTokenTypeAsync(object token, CancellationToken cancellationToken) - => GetTokenTypeAsync((TToken) token, cancellationToken); + ValueTask IOpenIddictTokenManager.GetTypeAsync(object token, CancellationToken cancellationToken) + => GetTypeAsync((TToken) token, cancellationToken); Task IOpenIddictTokenManager.IsRedeemedAsync(object token, CancellationToken cancellationToken) => IsRedeemedAsync((TToken) token, cancellationToken); @@ -1246,9 +1410,6 @@ namespace OpenIddict.Core Task> IOpenIddictTokenManager.ListAsync(Func, TState, IQueryable> query, TState state, CancellationToken cancellationToken) => ListAsync(query, state, cancellationToken); - Task IOpenIddictTokenManager.ObfuscateReferenceIdAsync(string identifier, CancellationToken cancellationToken) - => ObfuscateReferenceIdAsync(identifier, cancellationToken); - Task IOpenIddictTokenManager.PopulateAsync(OpenIddictTokenDescriptor descriptor, object token, CancellationToken cancellationToken) => PopulateAsync(descriptor, (TToken) token, cancellationToken); diff --git a/src/OpenIddict.Core/OpenIddict.Core.csproj b/src/OpenIddict.Core/OpenIddict.Core.csproj index 45704cf9..57703ecf 100644 --- a/src/OpenIddict.Core/OpenIddict.Core.csproj +++ b/src/OpenIddict.Core/OpenIddict.Core.csproj @@ -19,6 +19,7 @@ + diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index 44aeca6d..da16ea8c 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -292,11 +292,10 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceApplicationManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceApplicationManager() where TManager : class - => ReplaceApplicationManager(typeof(TManager), lifetime); + => ReplaceApplicationManager(typeof(TManager)); /// /// Replace the default application manager by a custom manager derived @@ -305,10 +304,8 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceApplicationManager( - [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceApplicationManager([NotNull] Type type) { if (type == null) { @@ -330,8 +327,8 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(new ServiceDescriptor(type, type, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, type)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>), type)); } else @@ -340,9 +337,9 @@ namespace Microsoft.Extensions.DependencyInjection => provider.GetRequiredService(typeof(OpenIddictApplicationManager<>) .MakeGenericType(root.GenericTypeArguments[0])); - Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type)); } return this; @@ -389,11 +386,10 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceAuthorizationManager() where TManager : class - => ReplaceAuthorizationManager(typeof(TManager), lifetime); + => ReplaceAuthorizationManager(typeof(TManager)); /// /// Replace the default authorization manager by a custom manager derived @@ -402,10 +398,8 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceAuthorizationManager( - [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceAuthorizationManager([NotNull] Type type) { if (type == null) { @@ -427,8 +421,8 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(new ServiceDescriptor(type, type, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, type)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>), type)); } else @@ -437,9 +431,9 @@ namespace Microsoft.Extensions.DependencyInjection => provider.GetRequiredService(typeof(OpenIddictAuthorizationManager<>) .MakeGenericType(root.GenericTypeArguments[0])); - Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type)); } return this; @@ -487,9 +481,9 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The type of the custom manager. /// The . - public OpenIddictCoreBuilder ReplaceScopeManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceScopeManager() where TManager : class - => ReplaceScopeManager(typeof(TManager), lifetime); + => ReplaceScopeManager(typeof(TManager)); /// /// Replace the default scope manager by a custom manager @@ -498,10 +492,8 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceScopeManager( - [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceScopeManager([NotNull] Type type) { if (type == null) { @@ -523,8 +515,8 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(new ServiceDescriptor(type, type, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, type)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>), type)); } else @@ -533,9 +525,9 @@ namespace Microsoft.Extensions.DependencyInjection => provider.GetRequiredService(typeof(OpenIddictScopeManager<>) .MakeGenericType(root.GenericTypeArguments[0])); - Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type)); } return this; @@ -582,11 +574,10 @@ namespace Microsoft.Extensions.DependencyInjection /// must be either a non-generic or closed generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenManager(ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceTokenManager() where TManager : class - => ReplaceTokenManager(typeof(TManager), lifetime); + => ReplaceTokenManager(typeof(TManager)); /// /// Replace the default token manager by a custom manager @@ -595,10 +586,8 @@ namespace Microsoft.Extensions.DependencyInjection /// either a non-generic, a closed or an open generic service. /// /// The type of the custom manager. - /// The lifetime of the registered service. /// The . - public OpenIddictCoreBuilder ReplaceTokenManager( - [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + public OpenIddictCoreBuilder ReplaceTokenManager([NotNull] Type type) { if (type == null) { @@ -620,8 +609,8 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.Replace(new ServiceDescriptor(type, type, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, type)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>), type)); } else @@ -630,9 +619,9 @@ namespace Microsoft.Extensions.DependencyInjection => provider.GetRequiredService(typeof(OpenIddictTokenManager<>) .MakeGenericType(root.GenericTypeArguments[0])); - Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); - Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>) - .MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); + Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager)); + Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>) + .MakeGenericType(root.GenericTypeArguments[0]), type)); } return this; @@ -672,6 +661,26 @@ namespace Microsoft.Extensions.DependencyInjection return this; } + /// + /// Disables additional filtering so that the OpenIddict managers don't execute a second check + /// to ensure the results returned by the stores exactly match the specified query filters, + /// casing included. Additional filtering shouldn't be disabled except when the underlying + /// stores are guaranteed to execute case-sensitive filtering at the database level. + /// Disabling this feature MAY result in security vulnerabilities in the other cases. + /// + /// The . + public OpenIddictCoreBuilder DisableAdditionalFiltering() + => Configure(options => options.DisableAdditionalFiltering = true); + + /// + /// Disables the scoped entity caching applied by the OpenIddict managers. + /// Disabling entity caching may have a noticeable impact on the performance + /// of your application and result in multiple queries being sent by the stores. + /// + /// The . + public OpenIddictCoreBuilder DisableEntityCaching() + => Configure(options => options.DisableEntityCaching = true); + /// /// Configures OpenIddict to use the specified entity as the default application entity. /// @@ -779,5 +788,21 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DefaultTokenType = type); } + + /// + /// Configures OpenIddict to use the specified entity cache limit, + /// after which the internal cache is automatically compacted. + /// + /// The cache limit, in number of entries. + /// The . + public OpenIddictCoreBuilder SetEntityCacheLimit(int limit) + { + if (limit < 10) + { + throw new ArgumentException("The cache size cannot be less than 10.", nameof(limit)); + } + + return Configure(options => options.EntityCacheLimit = limit); + } } } diff --git a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs index 255cfd78..f0c5ce72 100644 --- a/src/OpenIddict.Core/OpenIddictCoreExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreExtensions.cs @@ -40,6 +40,11 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddScoped(typeof(OpenIddictScopeManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictTokenManager<>)); + builder.Services.TryAddScoped(typeof(IOpenIddictApplicationCache<>), typeof(OpenIddictApplicationCache<>)); + builder.Services.TryAddScoped(typeof(IOpenIddictAuthorizationCache<>), typeof(OpenIddictAuthorizationCache<>)); + builder.Services.TryAddScoped(typeof(IOpenIddictScopeCache<>), typeof(OpenIddictScopeCache<>)); + builder.Services.TryAddScoped(typeof(IOpenIddictTokenCache<>), typeof(OpenIddictTokenCache<>)); + builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); diff --git a/src/OpenIddict.Core/OpenIddictCoreOptions.cs b/src/OpenIddict.Core/OpenIddictCoreOptions.cs index eef1208e..c5b19ff3 100644 --- a/src/OpenIddict.Core/OpenIddictCoreOptions.cs +++ b/src/OpenIddict.Core/OpenIddictCoreOptions.cs @@ -36,5 +36,30 @@ namespace OpenIddict.Core /// used by the non-generic token manager and the server/validation services. /// public Type DefaultTokenType { get; set; } + + /// + /// Gets or sets a boolean indicating whether additional filtering should be disabled, + /// so that the OpenIddict managers don't execute a second check to ensure the results + /// returned by the stores exactly match the specified query filters, casing included. + /// This property SHOULD NOT be set to true except when the underlying stores + /// are guaranteed to execute case-sensitive filtering at the database level. + /// Disabling this feature MAY result in security vulnerabilities in the other cases. + /// + public bool DisableAdditionalFiltering { get; set; } + + /// + /// Gets or sets a boolean indicating whether entity caching should be disabled. + /// Disabling entity caching may have a noticeable impact on the performance + /// of your application and result in multiple queries being sent by the stores. + /// + public bool DisableEntityCaching { get; set; } + + /// + /// Gets or sets the maximum number of cached entries allowed. When the threshold + /// is reached, the cache is automatically compacted to ensure it doesn't grow + /// abnormally and doesn't cause a memory starvation or out-of-memory exceptions. + /// This property is not used when is true. + /// + public int EntityCacheLimit { get; set; } = 250; } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs index 5913dd68..f42f818c 100644 --- a/src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -14,9 +14,7 @@ namespace OpenIddict.Core private readonly IServiceProvider _provider; public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns an application store compatible with the specified application type or throws an diff --git a/src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs index e520a8eb..ad57df18 100644 --- a/src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -14,9 +14,7 @@ namespace OpenIddict.Core private readonly IServiceProvider _provider; public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns an authorization store compatible with the specified authorization type or throws an diff --git a/src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs index f40777b0..3d9efcbe 100644 --- a/src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs @@ -14,9 +14,7 @@ namespace OpenIddict.Core private readonly IServiceProvider _provider; public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns a scope store compatible with the specified scope type or throws an diff --git a/src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs index a7ed6b6f..e400ac5b 100644 --- a/src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs @@ -14,9 +14,7 @@ namespace OpenIddict.Core private readonly IServiceProvider _provider; public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns a token store compatible with the specified token type or throws an diff --git a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs index ccbc32e7..a1c3e3ea 100644 --- a/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs +++ b/src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs @@ -34,6 +34,11 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddMemoryCache(); + // Since Entity Framework 6.x may be used with databases performing case-insensitive + // or culture-sensitive comparisons, ensure the additional filtering logic is enforced + // in case case-sensitive stores were registered before this extension was called. + builder.Configure(options => options.DisableAdditionalFiltering = false); + builder.SetDefaultApplicationEntity() .SetDefaultAuthorizationEntity() .SetDefaultScopeEntity() diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs index a8942b8e..3a1de64d 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs @@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -282,7 +282,8 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByPostLogoutRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -298,7 +299,7 @@ namespace OpenIddict.EntityFramework where application.PostLogoutRedirectUris.Contains(address) select application).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(); + var builder = ImmutableArray.CreateBuilder(applications.Count); foreach (var application in applications) { @@ -329,7 +330,8 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified redirect_uri. /// - public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -345,7 +347,7 @@ namespace OpenIddict.EntityFramework where application.RedirectUris.Contains(address) select application).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(); + var builder = ImmutableArray.CreateBuilder(applications.Count); foreach (var application in applications) { @@ -602,7 +604,18 @@ namespace OpenIddict.EntityFramework return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(application.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("2e3e9680-5654-48d8-a27d-b8bb4f0f1d50", "\x1e", application.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(application.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs index cb1e2244..15549405 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -550,7 +550,18 @@ namespace OpenIddict.EntityFramework return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(authorization.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("68056e1a-dbcf-412b-9a6a-d791c7dbe726", "\x1e", authorization.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(authorization.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// @@ -574,7 +585,20 @@ namespace OpenIddict.EntityFramework return new ValueTask>(ImmutableArray.Create()); } - return new ValueTask>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); + // Note: parsing the stringified scopes is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("2ba4ab0f-e2ec-4d48-b3bd-28e2bb660c75", "\x1e", authorization.Scopes); + var scopes = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(authorization.Scopes) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(scopes); } /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs index 59dc25bb..0f657bae 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs @@ -61,7 +61,7 @@ namespace OpenIddict.EntityFramework } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -390,7 +390,18 @@ namespace OpenIddict.EntityFramework return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(scope.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("78d8dfdd-3870-442e-b62e-dc9bf6eaeff7", "\x1e", scope.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(scope.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs index 212e5a80..711e5ad4 100644 --- a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -615,7 +615,18 @@ namespace OpenIddict.EntityFramework return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(token.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("d0509397-1bbf-40e7-97e1-5e6d7bc2536c", "\x1e", token.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(token.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// @@ -686,7 +697,7 @@ namespace OpenIddict.EntityFramework /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs index 8a9a5214..4198589e 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -35,6 +35,11 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddMemoryCache(); + // Since Entity Framework Core may be used with databases performing case-insensitive + // or culture-sensitive comparisons, ensure the additional filtering logic is enforced + // in case case-sensitive stores were registered before this extension was called. + builder.Configure(options => options.DisableAdditionalFiltering = false); + builder.SetDefaultApplicationEntity() .SetDefaultAuthorizationEntity() .SetDefaultScopeEntity() diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 216049fe..8798d150 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -91,7 +91,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -353,15 +353,18 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByPostLogoutRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - var builder = ImmutableArray.CreateBuilder(); - foreach (var application in await FindByPostLogoutRedirectUri(Context, address).ToListAsync(cancellationToken)) + var applications = await FindByPostLogoutRedirectUri(Context, address).ToListAsync(cancellationToken); + var builder = ImmutableArray.CreateBuilder(applications.Count); + + foreach (var application in applications) { foreach (var uri in await GetPostLogoutRedirectUrisAsync(application, cancellationToken)) { @@ -405,16 +408,18 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified redirect_uri. /// - public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { throw new ArgumentException("The address cannot be null or empty.", nameof(address)); } - var builder = ImmutableArray.CreateBuilder(); + var applications = await FindByRedirectUri(Context, address).ToListAsync(cancellationToken); + var builder = ImmutableArray.CreateBuilder(applications.Count); - foreach (var application in await FindByRedirectUri(Context, address).ToListAsync(cancellationToken)) + foreach (var application in applications) { foreach (var uri in await GetRedirectUrisAsync(application, cancellationToken)) { @@ -669,7 +674,18 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(application.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("2e3e9680-5654-48d8-a27d-b8bb4f0f1d50", "\x1e", application.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(application.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs index 726f0ea3..dcd3627a 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -91,7 +91,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -651,7 +651,18 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(authorization.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("68056e1a-dbcf-412b-9a6a-d791c7dbe726", "\x1e", authorization.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(authorization.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// @@ -675,7 +686,20 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask>(ImmutableArray.Create()); } - return new ValueTask>(JArray.Parse(authorization.Scopes).Select(element => (string) element).ToImmutableArray()); + // Note: parsing the stringified scopes is an expensive operation. + // To mitigate that, the resulting array is stored in the memory cache. + var key = string.Concat("2ba4ab0f-e2ec-4d48-b3bd-28e2bb660c75", "\x1e", authorization.Scopes); + var scopes = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JArray.Parse(authorization.Scopes) + .Select(element => (string) element) + .ToImmutableArray(); + }); + + return new ValueTask>(scopes); } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs index 5777a949..f945e4f8 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -79,7 +79,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -431,7 +431,18 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(scope.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("78d8dfdd-3870-442e-b62e-dc9bf6eaeff7", "\x1e", scope.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(scope.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs index a500346f..630832f6 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -91,7 +91,7 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Gets the memory cached associated with the current store. + /// Gets the memory cache associated with the current store. /// protected IMemoryCache Cache { get; } @@ -729,7 +729,18 @@ namespace OpenIddict.EntityFrameworkCore return new ValueTask(new JObject()); } - return new ValueTask(JObject.Parse(token.Properties)); + // Note: parsing the stringified properties is an expensive operation. + // To mitigate that, the resulting object is stored in the memory cache. + var key = string.Concat("d0509397-1bbf-40e7-97e1-5e6d7bc2536c", "\x1e", token.Properties); + var properties = Cache.GetOrCreate(key, entry => + { + entry.SetPriority(CacheItemPriority.High) + .SetSlidingExpiration(TimeSpan.FromMinutes(1)); + + return JObject.Parse(token.Properties); + }); + + return new ValueTask((JObject) properties.DeepClone()); } /// @@ -800,7 +811,7 @@ namespace OpenIddict.EntityFrameworkCore /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { diff --git a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs index dafb7860..2cba60c4 100644 --- a/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs +++ b/src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs @@ -31,7 +31,9 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(builder)); } - builder.Services.AddMemoryCache(); + // Note: Mongo uses simple binary comparison checks by default so the additional + // query filtering applied by the default OpenIddict managers can be safely disabled. + builder.DisableAdditionalFiltering(); builder.SetDefaultApplicationEntity() .SetDefaultAuthorizationEntity() diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs index af6e765e..3f30d555 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -21,9 +21,7 @@ namespace OpenIddict.MongoDb private readonly IServiceProvider _provider; public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns an application store compatible with the specified application type or throws an diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs index c449eb12..7e6d6893 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -21,9 +21,7 @@ namespace OpenIddict.MongoDb private readonly IServiceProvider _provider; public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns an authorization store compatible with the specified authorization type or throws an diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs index f2674248..1d9671a1 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs @@ -21,9 +21,7 @@ namespace OpenIddict.MongoDb private readonly IServiceProvider _provider; public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns a scope store compatible with the specified scope type or throws an diff --git a/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs index fe99d150..9d5be3ed 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs @@ -21,9 +21,7 @@ namespace OpenIddict.MongoDb private readonly IServiceProvider _provider; public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) - { - _provider = provider; - } + => _provider = provider; /// /// Returns a token store compatible with the specified token type or throws an diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs index 6d8c72ad..289c97dc 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs @@ -12,7 +12,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; @@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb where TApplication : OpenIddictApplication { public OpenIddictApplicationStore( - [NotNull] IMemoryCache cache, [NotNull] IOpenIddictMongoDbContext context, [NotNull] IOptionsMonitor options) { - Cache = cache; Context = context; Options = options; } - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - /// /// Gets the database context associated with the current store. /// @@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of applications that match the specified query. /// - public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual async Task CountAsync( + [NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -193,7 +186,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.ApplicationsCollectionName); - return await collection.Find(application => application.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + return await collection.Find(application => application.Id == + ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); } /// @@ -205,7 +199,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public virtual async Task> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByPostLogoutRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -228,7 +223,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, whose result /// returns the client applications corresponding to the specified redirect_uri. /// - public virtual async Task> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) + public virtual async Task> FindByRedirectUriAsync( + [NotNull] string address, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(address)) { @@ -394,7 +390,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns all the permissions associated with the application. /// - public virtual ValueTask> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetPermissionsAsync( + [NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -418,7 +415,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns all the post_logout_redirect_uri associated with the application. /// - public virtual ValueTask> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetPostLogoutRedirectUrisAsync( + [NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { @@ -466,7 +464,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns all the redirect_uri associated with the application. /// - public virtual ValueTask> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) + public virtual ValueTask> GetRedirectUrisAsync( + [NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs index d35c75f2..9f7053c9 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs @@ -12,7 +12,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; @@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb where TAuthorization : OpenIddictAuthorization { public OpenIddictAuthorizationStore( - [NotNull] IMemoryCache cache, [NotNull] IOpenIddictMongoDbContext context, [NotNull] IOptionsMonitor options) { - Cache = cache; Context = context; Options = options; } - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - /// /// Gets the database context associated with the current store. /// @@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of authorizations that match the specified query. /// - public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual async Task CountAsync( + [NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -357,7 +350,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); - return await collection.Find(authorization => authorization.Id == ObjectId.Parse(identifier)).FirstOrDefaultAsync(cancellationToken); + return await collection.Find(authorization => authorization.Id == ObjectId.Parse(identifier)) + .FirstOrDefaultAsync(cancellationToken); } /// @@ -380,7 +374,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.AuthorizationsCollectionName); - return ImmutableArray.CreateRange(await collection.Find(authorization => authorization.Subject == subject).ToListAsync(cancellationToken)); + return ImmutableArray.CreateRange(await collection.Find(authorization => + authorization.Subject == subject).ToListAsync(cancellationToken)); } /// @@ -481,7 +476,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the scopes associated with the specified authorization. /// - public virtual ValueTask> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) + public virtual ValueTask> GetScopesAsync( + [NotNull] TAuthorization authorization, CancellationToken cancellationToken) { if (authorization == null) { diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs index cbee7399..d1bcc5b0 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs @@ -12,7 +12,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; @@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb where TScope : OpenIddictScope { public OpenIddictScopeStore( - [NotNull] IMemoryCache cache, [NotNull] IOpenIddictMongoDbContext context, [NotNull] IOptionsMonitor options) { - Cache = cache; Context = context; Options = options; } - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - /// /// Gets the database context associated with the current store. /// @@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of scopes that match the specified query. /// - public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual async Task CountAsync( + [NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -231,7 +224,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.ScopesCollectionName); - return ImmutableArray.CreateRange(await collection.Find(scope => scope.Resources.Contains(resource)).ToListAsync(cancellationToken)); + return ImmutableArray.CreateRange(await collection.Find(scope => + scope.Resources.Contains(resource)).ToListAsync(cancellationToken)); } /// diff --git a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs index a7422ff9..1be0ab5e 100644 --- a/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs @@ -12,7 +12,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using MongoDB.Bson; using MongoDB.Driver; @@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb where TToken : OpenIddictToken { public OpenIddictTokenStore( - [NotNull] IMemoryCache cache, [NotNull] IOpenIddictMongoDbContext context, [NotNull] IOptionsMonitor options) { - Cache = cache; Context = context; Options = options; } - /// - /// Gets the memory cached associated with the current store. - /// - protected IMemoryCache Cache { get; } - /// /// Gets the database context associated with the current store. /// @@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the number of tokens that match the specified query. /// - public virtual async Task CountAsync([NotNull] Func, IQueryable> query, CancellationToken cancellationToken) + public virtual async Task CountAsync( + [NotNull] Func, IQueryable> query, CancellationToken cancellationToken) { if (query == null) { @@ -270,7 +263,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified application. /// - public virtual async Task> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual async Task> FindByApplicationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -280,7 +274,8 @@ namespace OpenIddict.MongoDb var database = await Context.GetDatabaseAsync(cancellationToken); var collection = database.GetCollection(Options.CurrentValue.TokensCollectionName); - return ImmutableArray.CreateRange(await collection.Find(token => token.ApplicationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken)); + return ImmutableArray.CreateRange(await collection.Find(token => + token.ApplicationId == ObjectId.Parse(identifier)).ToListAsync(cancellationToken)); } /// @@ -292,7 +287,8 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens corresponding to the specified authorization. /// - public virtual async Task> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) + public virtual async Task> FindByAuthorizationIdAsync( + [NotNull] string identifier, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(identifier)) { @@ -605,7 +601,7 @@ namespace OpenIddict.MongoDb /// A that can be used to monitor the asynchronous operation, /// whose result returns the token type associated with the specified token. /// - public virtual ValueTask GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) + public virtual ValueTask GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) { if (token == null) { diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs index 9d1dd9b9..719b73d5 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs @@ -301,10 +301,6 @@ namespace OpenIddict.Server.Internal return; } - // Store the application entity as a request property to make it accessible - // from the other provider methods without having to call the store twice. - context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); - // To prevent downgrade attacks, ensure that authorization requests returning an access token directly // from the authorization endpoint are rejected if the client_id corresponds to a confidential application. // Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs index dde4b5f8..d08c94e9 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -169,10 +169,6 @@ namespace OpenIddict.Server.Internal return; } - // Store the application entity as a request property to make it accessible - // from the other provider methods without having to call the store twice. - context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); - // Reject the request if the application is not allowed to use the token endpoint. if (!options.IgnoreEndpointPermissions && !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Token)) @@ -348,16 +344,16 @@ namespace OpenIddict.Server.Internal var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId); Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - // Retrieve the authorization code/refresh token from the request properties. - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - Debug.Assert(token != null, "The token shouldn't be null."); - // If the authorization code/refresh token is already marked as redeemed, this may indicate that // it was compromised. In this case, revoke the authorization and all the associated tokens. // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. - if (await _tokenManager.IsRedeemedAsync(token)) + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null || await _tokenManager.IsRedeemedAsync(token)) { - await TryRevokeTokenAsync(token); + if (token != null) + { + await TryRevokeTokenAsync(token); + } // Try to revoke the authorization and the associated tokens. // If the operation fails, the helpers will automatically log diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index 4bfb1d34..54c055ea 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -52,8 +52,11 @@ namespace OpenIddict.Server.Internal // If the client application is known, bind it to the authorization. if (!string.IsNullOrEmpty(request.ClientId)) { - var application = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}"); - Debug.Assert(application != null, "The client application shouldn't be null."); + var application = await _applicationManager.FindByClientIdAsync(request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } @@ -129,8 +132,6 @@ namespace OpenIddict.Server.Internal descriptor.Properties.Add(property); } - string result = null; - // When reference tokens are enabled or when the token is an authorization code or a // refresh token, remove the unnecessary properties from the authentication ticket. if (options.UseReferenceTokens || @@ -155,10 +156,10 @@ namespace OpenIddict.Server.Internal // substituted to the ciphertext returned by the data format. var bytes = new byte[256 / 8]; options.RandomNumberGenerator.GetBytes(bytes); - result = Base64UrlEncoder.Encode(bytes); - // Obfuscate the reference identifier so it can be safely stored in the databse. - descriptor.ReferenceId = await _tokenManager.ObfuscateReferenceIdAsync(result); + // Note: the default token manager automatically obfuscates the + // reference identifier so it can be safely stored in the databse. + descriptor.ReferenceId = Base64UrlEncoder.Encode(bytes); } // Otherwise, only create a token metadata entry for authorization codes and refresh tokens. @@ -171,8 +172,11 @@ namespace OpenIddict.Server.Internal // If the client application is known, associate it with the token. if (!string.IsNullOrEmpty(request.ClientId)) { - var application = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}"); - Debug.Assert(application != null, "The client application shouldn't be null."); + var application = await _applicationManager.FindByClientIdAsync(request.ClientId); + if (application == null) + { + throw new InvalidOperationException("The application entry cannot be found in the database."); + } descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } @@ -202,14 +206,16 @@ namespace OpenIddict.Server.Internal ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, identifier) .SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, descriptor.AuthorizationId); - if (!string.IsNullOrEmpty(result)) + if (options.UseReferenceTokens) { _logger.LogTrace("A new reference token was successfully generated and persisted " + "in the database: {Token} ; {Claims} ; {Properties}.", - result, ticket.Principal.Claims, ticket.Properties.Items); + descriptor.ReferenceId, ticket.Principal.Claims, ticket.Properties.Items); + + return descriptor.ReferenceId; } - return result; + return null; } private async Task ReceiveTokenAsync( @@ -232,30 +238,31 @@ namespace OpenIddict.Server.Internal if (options.UseReferenceTokens) { - // For introspection or revocation requests, this method may be called more than once. - // For reference tokens, this may result in multiple database calls being made. - // To optimize that, the token is added to the request properties to indicate that - // a database lookup was already made with the same identifier. If the marker exists, - // the property value (that may be null) is used instead of making a database call. - if (request.HasProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}")) + token = await _tokenManager.FindByReferenceIdAsync(value); + if (token == null) { - token = request.GetProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}"); + _logger.LogInformation("The reference token corresponding to the '{Identifier}' " + + "reference identifier cannot be found in the database.", value); + + return null; } - else + // Optimization: avoid extracting/decrypting the token payload + // (that relies on a format specific to the token type requested) + // if the token type associated with the token entry isn't valid. + var usage = await _tokenManager.GetTypeAsync(token); + if (string.IsNullOrEmpty(usage)) { - // Retrieve the token entry from the database. If it - // cannot be found, assume the token is not valid. - token = await _tokenManager.FindByReferenceIdAsync(value); + _logger.LogWarning("The token type associated with the received token cannot be retrieved. " + + "This may indicate that the token entry is corrupted."); - // Store the token as a request property so it can be retrieved if this method is called another time. - request.AddProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}", token); + return null; } - if (token == null) + if (!string.Equals(usage, type, StringComparison.OrdinalIgnoreCase)) { - _logger.LogInformation("The reference token corresponding to the '{Identifier}' " + - "reference identifier cannot be found in the database.", value); + _logger.LogWarning("The token type '{ActualType}' associated with the database entry doesn't match " + + "the expected type: {ExpectedType}.", await _tokenManager.GetTypeAsync(token), type); return null; } @@ -289,8 +296,6 @@ namespace OpenIddict.Server.Internal return null; } - - request.SetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token); } else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || @@ -313,26 +318,7 @@ namespace OpenIddict.Server.Internal return null; } - // For introspection or revocation requests, this method may be called more than once. - // For codes/refresh tokens, this may result in multiple database calls being made. - // To optimize that, the token is added to the request properties to indicate that - // a database lookup was already made with the same identifier. If the marker exists, - // the property value (that may be null) is used instead of making a database call. - if (request.HasProperty($"{OpenIddictConstants.Properties.Token}:{identifier}")) - { - token = request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - } - - // Otherwise, retrieve the authorization code/refresh token entry from the database. - // If it cannot be found, assume the authorization code/refresh token is not valid. - else - { - token = await _tokenManager.FindByIdAsync(identifier); - - // Store the token as a request property so it can be retrieved if this method is called another time. - request.AddProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token); - } - + token = await _tokenManager.FindByIdAsync(identifier); if (token == null) { _logger.LogInformation("The token '{Identifier}' cannot be found in the database.", identifier); diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs index 1eee8d8a..1645351e 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.Logging; @@ -56,10 +55,6 @@ namespace OpenIddict.Server.Internal return; } - // Store the application entity as a request property to make it accessible - // from the other provider methods without having to call the store twice. - context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); - // Reject the request if the application is not allowed to use the introspection endpoint. if (!options.IgnoreEndpointPermissions && !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Introspection)) @@ -171,13 +166,11 @@ namespace OpenIddict.Server.Internal // which an entry exists in the database - ensure it is still valid. if (options.UseReferenceTokens) { - // Retrieve the token from the request properties. If it's marked as invalid, return active = false. - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - Debug.Assert(token != null, "The token shouldn't be null."); - - if (!await _tokenManager.IsValidAsync(token)) + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null || !await _tokenManager.IsValidAsync(token)) { - _logger.LogInformation("The token '{Identifier}' was declared as inactive because it was revoked.", identifier); + _logger.LogInformation("The token '{Identifier}' was declared as inactive because it was " + + "not found in the database or was no longer valid.", identifier); context.Active = false; diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs index 1f56b26d..d8aeaa3c 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs @@ -97,10 +97,6 @@ namespace OpenIddict.Server.Internal return; } - // Store the application entity as a request property to make it accessible - // from the other provider methods without having to call the store twice. - context.Request.SetProperty($"{OpenIddictConstants.Properties.Application}:{context.ClientId}", application); - // Reject the request if the application is not allowed to use the revocation endpoint. if (!options.IgnoreEndpointPermissions && !await _applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Revocation)) @@ -206,11 +202,8 @@ namespace OpenIddict.Server.Internal var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId); Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); - // Retrieve the token from the request properties. If it's already marked as revoked, directly return a 200 response. - var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); - Debug.Assert(token != null, "The token shouldn't be null."); - - if (await _tokenManager.IsRevokedAsync(token)) + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null || await _tokenManager.IsRevokedAsync(token)) { _logger.LogInformation("The token '{Identifier}' was not revoked because " + "it was already marked as invalid.", identifier); diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs index ed601d49..1929438a 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -147,9 +147,17 @@ namespace OpenIddict.Server.Internal // If token revocation was explicitly disabled, none of the following security routines apply. if (!options.DisableTokenStorage) { - var token = context.Request.GetProperty(OpenIddictConstants.Properties.Token + ":" + - context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId)); - Debug.Assert(token != null, "The token shouldn't be null."); + var token = await _tokenManager.FindByIdAsync(context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId)); + if (token == null) + { + context.Reject( + error: OpenIddictConstants.Errors.InvalidGrant, + description: context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + "The specified refresh token is no longer valid."); + + return; + } // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, // mark the authorization code or the refresh token as redeemed to prevent future reuses. diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs index a91e7697..664fc628 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs @@ -5,6 +5,7 @@ */ using System; +using System.Diagnostics; using System.Text; using System.Threading.Tasks; using AspNet.Security.OAuth.Validation; @@ -64,20 +65,28 @@ namespace OpenIddict.Validation.Internal return; } - // Extract the encrypted payload from the token. If it's null or empty, - // assume the token is not a reference token and consider it as invalid. - var payload = await manager.GetPayloadAsync(token); - if (string.IsNullOrEmpty(payload)) + // Optimization: avoid extracting/decrypting the token payload if the token is not an access token. + var type = await manager.GetTypeAsync(token); + if (string.IsNullOrEmpty(type)) { - context.Fail("Authentication failed because the access token is not a reference token."); + context.Fail("Authentication failed because the token type associated with the entry is missing."); return; } - // Ensure the access token is still valid (i.e was not marked as revoked). - if (!await manager.IsValidAsync(token)) + if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase)) { - context.Fail("Authentication failed because the access token was no longer valid."); + context.Fail("Authentication failed because the specified token is not an access token."); + + return; + } + + // Extract the encrypted payload from the token. If it's null or empty, + // assume the token is not a reference token and consider it as invalid. + var payload = await manager.GetPayloadAsync(token); + if (string.IsNullOrEmpty(payload)) + { + context.Fail("Authentication failed because the access token is not a reference token."); return; } @@ -114,6 +123,33 @@ namespace OpenIddict.Validation.Internal public override async Task ValidateToken([NotNull] ValidateTokenContext context) { var options = (OpenIddictValidationOptions) context.Options; + if (options.UseReferenceTokens) + { + // Note: the token manager is deliberately not injected using constructor injection + // to allow using the validation handler without having to register the core services. + var manager = context.HttpContext.RequestServices.GetService(); + if (manager == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling reference tokens support.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .Append("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .ToString()); + } + + var identifier = context.Properties.GetProperty(OpenIddictConstants.Properties.InternalTokenId); + Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); + + // Ensure the access token is still valid (i.e was not marked as revoked). + var token = await manager.FindByIdAsync(identifier); + if (token == null || !await manager.IsValidAsync(token)) + { + context.Fail("Authentication failed because the access token was no longer valid."); + + return; + } + } + if (options.EnableAuthorizationValidation) { // Note: the authorization manager is deliberately not injected using constructor injection diff --git a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs index bf45cc62..573528ad 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs @@ -391,6 +391,40 @@ namespace OpenIddict.Core.Tests Assert.IsType(type, store); } + [Fact] + public void DisableAdditionalFiltering_FilteringIsCorrectlyDisabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.DisableAdditionalFiltering(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.True(options.DisableAdditionalFiltering); + } + + [Fact] + public void DisableEntityCaching_CachingIsCorrectlyDisabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.DisableEntityCaching(); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.True(options.DisableEntityCaching); + } + [Fact] public void SetDefaultApplicationEntity_ThrowsAnExceptionForNullType() { @@ -591,6 +625,40 @@ namespace OpenIddict.Core.Tests Assert.Equal(typeof(CustomToken), options.DefaultTokenType); } + [Theory] + [InlineData(-10)] + [InlineData(0)] + [InlineData(9)] + public void SetEntityCacheLimit_ThrowsAnExceptionForInvalidLimit(int limit) + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(() => builder.SetEntityCacheLimit(limit)); + + Assert.Equal("limit", exception.ParamName); + Assert.StartsWith("The cache size cannot be less than 10.", exception.Message); + } + + [Fact] + public void SetEntityCacheLimit_LimitIsCorrectlyDisabled() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.SetEntityCacheLimit(42); + + // Assert + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>().CurrentValue; + + Assert.Equal(42, options.EntityCacheLimit); + } + private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services) => services.AddOpenIddict().AddCore(); @@ -610,10 +678,11 @@ namespace OpenIddict.Core.Tests private class ClosedGenericApplicationManager : OpenIddictApplicationManager { public ClosedGenericApplicationManager( + IOpenIddictApplicationCache cache, IOpenIddictApplicationStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -622,10 +691,11 @@ namespace OpenIddict.Core.Tests where TApplication : class { public OpenGenericApplicationManager( + IOpenIddictApplicationCache cache, IOpenIddictApplicationStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -633,10 +703,11 @@ namespace OpenIddict.Core.Tests private class ClosedGenericAuthorizationManager : OpenIddictAuthorizationManager { public ClosedGenericAuthorizationManager( + IOpenIddictAuthorizationCache cache, IOpenIddictAuthorizationStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -645,10 +716,11 @@ namespace OpenIddict.Core.Tests where TAuthorization : class { public OpenGenericAuthorizationManager( + IOpenIddictAuthorizationCache cache, IOpenIddictAuthorizationStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -656,10 +728,11 @@ namespace OpenIddict.Core.Tests private class ClosedGenericScopeManager : OpenIddictScopeManager { public ClosedGenericScopeManager( + IOpenIddictScopeCache cache, IOpenIddictScopeStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -668,10 +741,11 @@ namespace OpenIddict.Core.Tests where TScope : class { public OpenGenericScopeManager( + IOpenIddictScopeCache cache, IOpenIddictScopeStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -679,10 +753,11 @@ namespace OpenIddict.Core.Tests private class ClosedGenericTokenManager : OpenIddictTokenManager { public ClosedGenericTokenManager( + IOpenIddictTokenCache cache, IOpenIddictTokenStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } @@ -691,10 +766,11 @@ namespace OpenIddict.Core.Tests where TToken : class { public OpenGenericTokenManager( + IOpenIddictTokenCache cache, IOpenIddictTokenStoreResolver resolver, ILogger> logger, IOptionsMonitor options) - : base(resolver, logger, options) + : base(cache, resolver, logger, options) { } } diff --git a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs index fa7bc1fd..4ee87417 100644 --- a/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs +++ b/test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs @@ -42,20 +42,6 @@ namespace OpenIddict.MongoDb.Tests Assert.Equal("configuration", exception.ParamName); } - [Fact] - public void UseMongoDb_RegistersCachingServices() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictCoreBuilder(services); - - // Act - builder.UseMongoDb(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IMemoryCache)); - } - [Fact] public void UseMongoDb_RegistersDefaultEntities() { diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs index b605332f..685f5e4a 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs @@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests private static OpenIddictApplicationStore CreateStore() => new Mock>( - Mock.Of(), Mock.Of(), Mock.Of>()).Object; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs index c27d672d..709212b8 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs @@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests private static OpenIddictAuthorizationStore CreateStore() => new Mock>( - Mock.Of(), Mock.Of(), Mock.Of>()).Object; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs index 95c3fdcd..993021bf 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs @@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests private static OpenIddictScopeStore CreateStore() => new Mock>( - Mock.Of(), Mock.Of(), Mock.Of>()).Object; diff --git a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs index 06b9c7ca..9993f2b0 100644 --- a/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs +++ b/test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs @@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests private static OpenIddictTokenStore CreateStore() => new Mock>( - Mock.Of(), Mock.Of(), Mock.Of>()).Object; diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs index 0a11ec0c..c0aabcad 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -990,7 +990,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); } @@ -1056,7 +1056,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); } @@ -1314,7 +1314,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); @@ -1407,7 +1407,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); @@ -1482,7 +1482,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } @@ -1552,7 +1552,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); } diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs index ea88bb57..19ab26b9 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs @@ -412,7 +412,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny()), Times.AtLeastOnce()); } [Fact] @@ -465,6 +465,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -556,6 +559,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -651,6 +657,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -720,6 +729,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs index cf176e2b..7143b936 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs @@ -473,7 +473,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.Empty(response.GetParameters()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), Times.Never()); } @@ -522,7 +522,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.Empty(response.GetParameters()); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once()); } } diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs index 7948964a..642f3576 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs @@ -142,6 +142,116 @@ namespace OpenIddict.Server.Internal.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); } + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForMissingTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIddictConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAccessToken_ReturnsNullForIncompatibleTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(IntrospectionEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Token = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + TokenTypeHint = OpenIdConnectConstants.TokenTypeHints.AccessToken + }); + + // Assert + Assert.Single(response.GetParameters()); + Assert.False((bool) response[OpenIddictConstants.Claims.Active]); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + [Fact] public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenIdentifier() { @@ -153,6 +263,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask(result: null)); }); @@ -193,7 +306,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); } @@ -208,6 +321,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -251,7 +367,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); } @@ -271,6 +387,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -316,7 +435,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIddictConstants.Claims.Active]); - Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.AtLeastOnce()); format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); } @@ -349,6 +468,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -543,6 +665,118 @@ namespace OpenIddict.Server.Internal.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); } + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForMissingTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeAuthorizationCode_ReturnsNullForIncompatibleTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AccessToken)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(CreateApplicationManager(instance => + { + var application = new OpenIddictApplication(); + + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.ClientTypes.Confidential)); + + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + .ReturnsAsync(true); + })); + + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + ClientId = "Fabrikam", + ClientSecret = "7Fjfp0ZBr1KtDRbnfVdmIw", + Code = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", + GrantType = OpenIddictConstants.GrantTypes.AuthorizationCode, + RedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified authorization code is invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + [Fact] public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenIdentifier() { @@ -554,6 +788,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask(result: null)); }); @@ -610,6 +847,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -674,6 +914,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -751,6 +994,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -1001,7 +1247,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.AccessToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); } @@ -1108,6 +1354,84 @@ namespace OpenIddict.Server.Internal.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny(), It.IsAny()), Times.Never()); } + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForMissingTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIddictConstants.GrantTypes.RefreshToken, + RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" + }); + + // Assert + Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + + [Fact] + public async Task DeserializeRefreshToken_ReturnsNullForIncompatibleTokenType() + { + // Arrange + var token = new OpenIddictToken(); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.AuthorizationCode)); + }); + + var server = CreateAuthorizationServer(builder => + { + builder.Services.AddSingleton(manager); + + builder.UseReferenceTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(TokenEndpoint, new OpenIdConnectRequest + { + GrantType = OpenIddictConstants.GrantTypes.RefreshToken, + RefreshToken = "HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ" + }); + + // Assert + Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); + Assert.Equal("The specified refresh token is invalid.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.AtLeastOnce()); + } + [Fact] public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenIdentifier() { @@ -1119,6 +1443,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask(result: null)); }); @@ -1158,6 +1485,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -1205,6 +1535,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -1263,6 +1596,9 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIdConnectConstants.TokenUsages.RefreshToken)); + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); @@ -1444,7 +1780,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.AccessToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); } @@ -1493,9 +1829,6 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1"); }); var server = CreateAuthorizationServer(builder => @@ -1526,9 +1859,6 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.AccessToken); - Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync( - It.IsAny(), It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && @@ -1779,9 +2109,6 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1"); }); var server = CreateAuthorizationServer(builder => @@ -1828,9 +2155,6 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.Code); - Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync( - It.IsAny(), It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && @@ -2136,9 +2460,6 @@ namespace OpenIddict.Server.Internal.Tests instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) .Returns(new ValueTask("3E228451-1555-46F7-A471-951EFBA23A56")); - - instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1"); }); var server = CreateAuthorizationServer(builder => @@ -2169,15 +2490,12 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync( - It.IsAny(), It.IsAny()), Times.Exactly(2)); - Mock.Get(manager).Verify(mock => mock.CreateAsync( It.Is(descriptor => descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && descriptor.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && descriptor.Payload != null && - descriptor.ReferenceId == "B1F0D503-55A4-4B03-B05B-EF07713C18E1" && + descriptor.ReferenceId != null && descriptor.Subject == "Bob le Magnifique" && descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), It.IsAny()), Times.Once()); diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs index 559acbc1..62707c8b 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -473,7 +473,7 @@ namespace OpenIddict.Server.Internal.Tests }); // Assert - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -548,7 +548,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -614,7 +614,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -684,7 +684,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Once()); } @@ -742,7 +742,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.Null(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny()), Times.Never()); } @@ -834,7 +834,7 @@ namespace OpenIddict.Server.Internal.Tests // Assert Assert.NotNull(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Once()); @@ -922,7 +922,7 @@ namespace OpenIddict.Server.Internal.Tests Assert.NotNull(response.AccessToken); Assert.Null(response.RefreshToken); - Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny()), Times.Never()); @@ -1707,6 +1707,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1720,6 +1721,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1733,6 +1735,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1746,6 +1749,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs index b13f0a8b..281de1c9 100644 --- a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs @@ -94,7 +94,7 @@ namespace OpenIddict.Validation.Internal.Tests } [Fact] - public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() + public async Task DecryptToken_ReturnsFailedResultForMissingTokenType() { // Arrange var token = new OpenIddictToken(); @@ -104,7 +104,7 @@ namespace OpenIddict.Validation.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) .ReturnsAsync(token); - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) .Returns(new ValueTask(result: null)); }); @@ -125,29 +125,60 @@ namespace OpenIddict.Validation.Internal.Tests Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.Once()); } [Fact] - public async Task DecryptToken_ReturnsFailedResultForReferenceTokenWithInvalidStatus() + public async Task DecryptToken_ReturnsFailedResultForIncompatibleTokenType() { // Arrange var token = new OpenIddictToken(); - var format = new Mock>(); - format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) - .Returns(value: null); + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.TokenTypes.RefreshToken)); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() + { + // Arrange + var token = new OpenIddictToken(); var manager = CreateTokenManager(instance => { instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) .ReturnsAsync(token); - instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) - .Returns(new ValueTask("valid-reference-token-payload")); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(false); + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask(result: null)); }); var server = CreateResourceServer(builder => @@ -168,8 +199,6 @@ namespace OpenIddict.Validation.Internal.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); - format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Never()); } [Fact] @@ -187,11 +216,11 @@ namespace OpenIddict.Validation.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) .Returns(new ValueTask("invalid-reference-token-payload")); - - instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); }); var server = CreateResourceServer(builder => @@ -213,12 +242,11 @@ namespace OpenIddict.Validation.Internal.Tests Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); format.Verify(mock => mock.Unprotect("invalid-reference-token-payload"), Times.Once()); } [Fact] - public async Task DecryptToken_ReturnsValidResultForValidReferenceToken() + public async Task ValidateToken_ReturnsFailedResultForInvalidReferenceToken() { // Arrange var token = new OpenIddictToken(); @@ -240,17 +268,97 @@ namespace OpenIddict.Validation.Internal.Tests instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) .ReturnsAsync(token); + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) .Returns(new ValueTask("valid-reference-token-payload")); + instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); + + instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) + .Returns(new ValueTask(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("4392E01A-1BC4-4776-8450-EC267C2B708A")); + + instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny())) + .ReturnsAsync(token); + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) - .ReturnsAsync(true); + .ReturnsAsync(false); + }); + + var server = CreateResourceServer(builder => + { + builder.Services.AddSingleton(manager); + builder.Configure(options => options.AccessTokenFormat = format.Object); + }); + + var client = server.CreateClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "/ticket"); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", "valid-reference-token-id"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + + Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny()), Times.Once()); + format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); + } + + [Fact] + public async Task ValidateToken_ReturnsValidResultForValidReferenceToken() + { + // Arrange + var token = new OpenIddictToken(); + + var format = new Mock>(); + format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) + .Returns(delegate + { + var identity = new ClaimsIdentity(OpenIddictValidationDefaults.AuthenticationScheme); + identity.AddClaim(new Claim(OAuthValidationConstants.Claims.Subject, "Fabrikam")); + + return new AuthenticationTicket( + new ClaimsPrincipal(identity), + OpenIddictValidationDefaults.AuthenticationScheme); + }); + + var manager = CreateTokenManager(instance => + { + instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny())) + .Returns(new ValueTask(OpenIddictConstants.TokenTypes.AccessToken)); + + instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny())) + .Returns(new ValueTask("valid-reference-token-payload")); instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny())) .Returns(new ValueTask(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny())) .Returns(new ValueTask(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); + + instance.Setup(mock => mock.GetIdAsync(token, It.IsAny())) + .Returns(new ValueTask("4392E01A-1BC4-4776-8450-EC267C2B708A")); + + instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny())) + .ReturnsAsync(token); + + instance.Setup(mock => mock.IsValidAsync(token, It.IsAny())) + .ReturnsAsync(true); }); var server = CreateResourceServer(builder => @@ -289,6 +397,7 @@ namespace OpenIddict.Validation.Internal.Tests Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny()), Times.Once()); format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); } @@ -596,9 +705,11 @@ namespace OpenIddict.Validation.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), - Mock.Of>()); + Mock.Of>(mock => + mock.CurrentValue == new OpenIddictCoreOptions())); configuration?.Invoke(manager); @@ -609,9 +720,11 @@ namespace OpenIddict.Validation.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), - Mock.Of>()); + Mock.Of>(mock => + mock.CurrentValue == new OpenIddictCoreOptions())); configuration?.Invoke(manager);