diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 39ab158e..b03a3411 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.EntityFrameworkCore; 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..2c2f0c48 --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs @@ -0,0 +1,309 @@ +/* + * 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 MemoryCache _cache; + private readonly IOpenIddictApplicationStore _store; + private readonly IOptions _options; + + public OpenIddictApplicationCache( + [NotNull] IOptions options, + [NotNull] IOpenIddictApplicationStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions()); + _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)); + } + + if (_cache.Count >= _options.Value.EntityCacheLimit) + { + _cache.Compact(0.25); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(application, cancellationToken) + })) + { + entry.SetValue(application); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByClientIdAsync), + Identifier = await _store.GetClientIdAsync(application, cancellationToken) + })) + { + 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.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.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.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.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..5c4e31fc --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs @@ -0,0 +1,505 @@ +/* + * 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 MemoryCache _cache; + private readonly IOpenIddictAuthorizationStore _store; + private readonly IOptions _options; + + public OpenIddictAuthorizationCache( + [NotNull] IOptions options, + [NotNull] IOpenIddictAuthorizationStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions()); + _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)); + } + + if (_cache.Count >= _options.Value.EntityCacheLimit) + { + _cache.Compact(0.25); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(authorization, cancellationToken) + })) + { + 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.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.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.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.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.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.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..435b18ea --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs @@ -0,0 +1,292 @@ +/* + * 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 MemoryCache _cache; + private readonly IOpenIddictScopeStore _store; + private readonly IOptions _options; + + public OpenIddictScopeCache( + [NotNull] IOptions options, + [NotNull] IOpenIddictScopeStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions()); + _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)); + } + + if (_cache.Count >= _options.Value.EntityCacheLimit) + { + _cache.Compact(0.25); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(scope, cancellationToken) + })) + { + entry.SetValue(scope); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByNameAsync), + Name = await _store.GetNameAsync(scope, cancellationToken) + })) + { + 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.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.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.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..9f9e4c2e --- /dev/null +++ b/src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs @@ -0,0 +1,562 @@ +/* + * 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 MemoryCache _cache; + private readonly IOpenIddictTokenStore _store; + private readonly IOptions _options; + + public OpenIddictTokenCache( + [NotNull] IOptions options, + [NotNull] IOpenIddictTokenStoreResolver resolver) + { + _cache = new MemoryCache(new MemoryCacheOptions()); + _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)); + } + + if (_cache.Count >= _options.Value.EntityCacheLimit) + { + _cache.Compact(0.25); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByIdAsync), + Identifier = await _store.GetIdAsync(token, cancellationToken) + })) + { + entry.SetValue(token); + } + + using (var entry = _cache.CreateEntry(new + { + Method = nameof(FindByReferenceIdAsync), + Identifier = await _store.GetReferenceIdAsync(token, cancellationToken) + })) + { + 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.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.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.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.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.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.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.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.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 d6dda21a..d14203c5 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] IOptions 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. @@ -175,7 +182,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); @@ -202,58 +209,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.Value.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.Value.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.Value.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.Value.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.Value.DisableAdditionalFiltering && + !string.Equals(await Store.GetIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal)) { return null; } @@ -278,16 +316,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.Value.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.Value.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) @@ -324,16 +370,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.Value.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.Value.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) @@ -366,6 +420,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); } @@ -402,7 +461,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) { @@ -441,7 +501,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 ValueTask GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) + public virtual ValueTask GetConsentTypeAsync( + [NotNull] TApplication application, CancellationToken cancellationToken = default) { if (application == null) { @@ -471,7 +532,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) { @@ -679,6 +741,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); } @@ -838,6 +905,11 @@ namespace OpenIddict.Core throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); } + if (!Options.Value.DisableEntityCaching) + { + await Cache.RemoveAsync(application, cancellationToken); + } + await Store.UpdateAsync(application, cancellationToken); } @@ -900,7 +972,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 79b78ec0..35bd1583 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] IOptions 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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.DisableEntityCaching ? + await Store.FindByApplicationIdAsync(identifier, cancellationToken) : + await Cache.FindByApplicationIdAsync(identifier, cancellationToken); + + if (authorizations.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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 7e481815..f2dfd5c9 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] IOptions 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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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 0a1ff9b1..a6f1c730 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] IOptions 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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.DisableEntityCaching ? + await Store.FindByApplicationIdAsync(identifier, cancellationToken) : + await Cache.FindByApplicationIdAsync(identifier, cancellationToken); + + if (tokens.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.Value.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.Value.DisableEntityCaching ? + await Store.FindByAuthorizationIdAsync(identifier, cancellationToken) : + await Cache.FindByAuthorizationIdAsync(identifier, cancellationToken); + + if (tokens.IsEmpty) + { + return ImmutableArray.Create(); + } + + if (Options.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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.Value.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 70acc746..e0752b81 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 9fdff98c..069d9038 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -293,11 +293,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 @@ -306,10 +305,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) { @@ -331,8 +328,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 @@ -341,9 +338,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; @@ -390,11 +387,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 @@ -403,10 +399,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) { @@ -428,8 +422,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 @@ -438,9 +432,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; @@ -488,9 +482,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 @@ -499,10 +493,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) { @@ -524,8 +516,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 @@ -534,9 +526,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; @@ -583,11 +575,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 @@ -596,10 +587,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) { @@ -621,8 +610,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 @@ -631,9 +620,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; @@ -673,6 +662,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. /// @@ -780,5 +789,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 d0dc50fe..85b84a9a 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 3f36f56b..07aeee14 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 24a9ea58..2c532a22 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; } @@ -556,7 +556,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()); } /// @@ -580,7 +591,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 512a1446..34181bbe 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 05f76e44..f4f3c42f 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; } @@ -627,7 +627,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()); } /// @@ -698,7 +709,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 cccdec71..4f45f1ef 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs @@ -36,6 +36,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 353a058a..326eecc3 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -90,7 +90,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; } @@ -325,7 +325,8 @@ 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)) { @@ -341,7 +342,7 @@ namespace OpenIddict.EntityFrameworkCore where application.PostLogoutRedirectUris.Contains(address) select application).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(); + var builder = ImmutableArray.CreateBuilder(applications.Count); foreach (var application in applications) { @@ -372,7 +373,8 @@ 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)) { @@ -388,7 +390,7 @@ namespace OpenIddict.EntityFrameworkCore where application.RedirectUris.Contains(address) select application).ToListAsync(cancellationToken); - var builder = ImmutableArray.CreateBuilder(); + var builder = ImmutableArray.CreateBuilder(applications.Count); foreach (var application in applications) { @@ -645,7 +647,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 8245cd25..789be893 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs @@ -90,7 +90,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; } @@ -606,7 +606,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()); } /// @@ -630,7 +641,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 e869d482..469e7f4b 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs @@ -78,7 +78,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; } @@ -407,7 +407,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 013890c2..9c944632 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs @@ -90,7 +90,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; } @@ -658,7 +658,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()); } /// @@ -729,7 +740,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 2d794080..fb86b347 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs @@ -22,9 +22,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 53eef9c6..672a8c2a 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs @@ -22,9 +22,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 5ec295a0..2174dbc1 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs @@ -22,9 +22,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 fa6229c7..f27195d8 100644 --- a/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs +++ b/src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs @@ -22,9 +22,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 dee64213..4605e180 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] IOptions 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.Value.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 5171f88a..cf2a2300 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] IOptions 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.Value.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.Value.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 5c094c1a..77496399 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] IOptions 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.Value.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 8029871e..e973eb73 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] IOptions 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.Value.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 da0e2af9..fffc981a 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs @@ -308,10 +308,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 f236bfe5..6a73ec98 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -175,10 +175,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)) @@ -360,16 +356,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, context.HttpContext); + if (token != null) + { + await TryRevokeTokenAsync(token, context.HttpContext); + } // 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 5c43a13b..5acb64f8 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -61,8 +61,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); } @@ -142,8 +145,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 || @@ -168,10 +169,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. @@ -184,8 +185,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); } @@ -215,14 +219,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( @@ -248,30 +254,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; } @@ -305,8 +312,6 @@ namespace OpenIddict.Server.Internal return null; } - - request.SetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token); } else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || @@ -329,26 +334,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 b241262e..b132bb76 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.DependencyInjection; @@ -61,10 +60,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)) @@ -181,13 +176,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 8248ea20..37b9dee7 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs @@ -102,10 +102,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)) @@ -215,11 +211,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 95293133..5900b727 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -51,6 +51,8 @@ namespace OpenIddict.Server.Internal { var options = (OpenIddictServerOptions) context.Options; + var tokenManager = GetTokenManager(context.HttpContext.RequestServices); + Debug.Assert(context.Request.IsAuthorizationRequest() || context.Request.IsTokenRequest(), "The request should be an authorization or token request."); @@ -120,9 +122,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 ec83d6d1..94cf6896 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; @@ -60,21 +61,30 @@ 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)) { - logger.LogError("Authentication failed because the access token is not a reference token."); + logger.LogError("Authentication failed because the token type associated with the entry is missing."); context.HandleResponse(); 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)) { - logger.LogError("Authentication failed because the access token was no longer valid."); + logger.LogError("Authentication failed because the specified token is not an access token."); + + context.HandleResponse(); + 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)) + { + logger.LogError("Authentication failed because the access token is not a reference token."); context.HandleResponse(); return; @@ -113,7 +123,37 @@ namespace OpenIddict.Validation.Internal public override async Task ValidateToken([NotNull] ValidateTokenContext context) { + var logger = GetLogger(context.HttpContext.RequestServices); + 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.Ticket.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)) + { + logger.LogError("Authentication failed because the access token was no longer valid."); + + context.Ticket = null; + return; + } + } + if (options.EnableAuthorizationValidation) { // Note: the authorization manager is deliberately not injected using constructor injection @@ -128,8 +168,6 @@ namespace OpenIddict.Validation.Internal .ToString()); } - var logger = GetLogger(context.HttpContext.RequestServices); - var identifier = context.Ticket.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId); if (!string.IsNullOrEmpty(identifier)) { diff --git a/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs b/test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs index 8d587d9d..2efdff30 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>().Value; + + 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>().Value; + + 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>().Value; + + 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, IOptions 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, IOptions 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, IOptions 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, IOptions 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, IOptions 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, IOptions 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, IOptions 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, IOptions 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 dbe70115..2053abd3 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 cc36f856..91d2046d 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 61150437..e35d6f5c 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 7ca52395..d29bcb5b 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 66844224..fd4ae894 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 eb3fbae4..dbacce57 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs @@ -992,7 +992,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()); } @@ -1058,7 +1058,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()); } @@ -1316,7 +1316,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()); @@ -1409,7 +1409,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()); @@ -1484,7 +1484,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()); } @@ -1554,7 +1554,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 500f04bd..c9e6dd67 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs @@ -413,7 +413,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] @@ -466,6 +466,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")); @@ -557,6 +560,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")); @@ -652,6 +658,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")); @@ -721,6 +730,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 d88530cb..aa39e292 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs @@ -475,7 +475,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()); } @@ -524,7 +524,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 2d540ae6..5b2ca236 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs @@ -144,6 +144,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() { @@ -155,6 +265,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)); }); @@ -195,7 +308,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()); } @@ -210,6 +323,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")); @@ -253,7 +369,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()); } @@ -273,6 +389,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")); @@ -318,7 +437,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()); } @@ -351,6 +470,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")); @@ -545,6 +667,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() { @@ -556,6 +790,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)); }); @@ -612,6 +849,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")); @@ -676,6 +916,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")); @@ -753,6 +996,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")); @@ -1003,7 +1249,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()); } @@ -1110,6 +1356,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() { @@ -1121,6 +1445,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)); }); @@ -1160,6 +1487,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")); @@ -1207,6 +1537,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")); @@ -1265,6 +1598,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")); @@ -1446,7 +1782,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()); } @@ -1495,9 +1831,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 => @@ -1528,9 +1861,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) && @@ -1781,9 +2111,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 => @@ -1830,9 +2157,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) && @@ -2138,9 +2462,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 => @@ -2171,15 +2492,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 2bf13125..11cc0ec4 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs @@ -474,7 +474,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()); } @@ -549,7 +549,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()); } @@ -615,7 +615,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()); } @@ -685,7 +685,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()); } @@ -743,7 +743,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()); } @@ -835,7 +835,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()); @@ -923,7 +923,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()); @@ -1576,6 +1576,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1589,6 +1590,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1602,6 +1604,7 @@ namespace OpenIddict.Server.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -1615,6 +1618,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 f67864f9..c97aa1fe 100644 --- a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs @@ -101,7 +101,7 @@ namespace OpenIddict.Validation.Internal.Tests } [Fact] - public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() + public async Task DecryptToken_ReturnsFailedResultForMissingTokenType() { // Arrange var token = new OpenIddictToken(); @@ -111,7 +111,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)); }); @@ -132,29 +132,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 => @@ -175,8 +206,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] @@ -194,11 +223,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 => @@ -220,12 +249,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(); @@ -248,17 +276,98 @@ 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), + new AuthenticationProperties(), + 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 => @@ -297,6 +406,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()); } @@ -620,6 +730,7 @@ namespace OpenIddict.Validation.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>()); @@ -633,6 +744,7 @@ namespace OpenIddict.Validation.Internal.Tests Action>> configuration = null) { var manager = new Mock>( + Mock.Of>(), Mock.Of(), Mock.Of>>(), Mock.Of>());