Browse Source

Backport the caching APIs to OpenIddict 1.x

pull/2236/head
Kévin Chalet 8 years ago
parent
commit
546cc5757c
  1. 1
      samples/Mvc.Server/Startup.cs
  2. 86
      src/OpenIddict.Abstractions/Caches/IOpenIddictApplicationCache.cs
  3. 135
      src/OpenIddict.Abstractions/Caches/IOpenIddictAuthorizationCache.cs
  4. 84
      src/OpenIddict.Abstractions/Caches/IOpenIddictScopeCache.cs
  5. 141
      src/OpenIddict.Abstractions/Caches/IOpenIddictTokenCache.cs
  6. 2
      src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs
  7. 13
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  8. 3
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  9. 2
      src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs
  10. 309
      src/OpenIddict.Core/Caches/OpenIddictApplicationCache.cs
  11. 505
      src/OpenIddict.Core/Caches/OpenIddictAuthorizationCache.cs
  12. 292
      src/OpenIddict.Core/Caches/OpenIddictScopeCache.cs
  13. 562
      src/OpenIddict.Core/Caches/OpenIddictTokenCache.cs
  14. 126
      src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs
  15. 186
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  16. 98
      src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs
  17. 303
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  18. 1
      src/OpenIddict.Core/OpenIddict.Core.csproj
  19. 111
      src/OpenIddict.Core/OpenIddictCoreBuilder.cs
  20. 5
      src/OpenIddict.Core/OpenIddictCoreExtensions.cs
  21. 25
      src/OpenIddict.Core/OpenIddictCoreOptions.cs
  22. 4
      src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs
  23. 4
      src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs
  24. 4
      src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs
  25. 4
      src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs
  26. 5
      src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs
  27. 25
      src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs
  28. 30
      src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs
  29. 15
      src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs
  30. 17
      src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs
  31. 5
      src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs
  32. 25
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs
  33. 30
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs
  34. 15
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs
  35. 17
      src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs
  36. 4
      src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs
  37. 4
      src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs
  38. 4
      src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs
  39. 4
      src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs
  40. 4
      src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs
  41. 29
      src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs
  42. 20
      src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs
  43. 14
      src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs
  44. 22
      src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs
  45. 4
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs
  46. 16
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs
  47. 86
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs
  48. 15
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs
  49. 11
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs
  50. 16
      src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs
  51. 58
      src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs
  52. 92
      test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs
  53. 14
      test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs
  54. 1
      test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs
  55. 1
      test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs
  56. 1
      test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs
  57. 1
      test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs
  58. 12
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Exchange.cs
  59. 14
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs
  60. 4
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs
  61. 366
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Serialization.cs
  62. 18
      test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs
  63. 150
      test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs

1
samples/Mvc.Server/Startup.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Primitives;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

86
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
{
/// <summary>
/// Provides methods allowing to cache applications after retrieving them from the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public interface IOpenIddictApplicationCache<TApplication> where TApplication : class
{
/// <summary>
/// Add the specified application to the cache.
/// </summary>
/// <param name="application">The application to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task AddAsync([NotNull] TApplication application, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
ValueTask<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified redirect_uri.
/// </returns>
ValueTask<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified redirect_uri.
/// </returns>
ValueTask<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken);
/// <summary>
/// Removes the specified application from the cache.
/// </summary>
/// <param name="application">The application to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task RemoveAsync([NotNull] TApplication application, CancellationToken cancellationToken);
}
}

135
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
{
/// <summary>
/// Provides methods allowing to cache authorizations after retrieving them from the store.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
public interface IOpenIddictAuthorizationCache<TAuthorization> where TAuthorization : class
{
/// <summary>
/// Add the specified authorization to the cache.
/// </summary>
/// <param name="authorization">The authorization to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task AddAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, [NotNull] string status, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, [NotNull] string status,
[NotNull] string type, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client, [NotNull] string status,
[NotNull] string type, ImmutableArray<string> scopes, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of authorizations corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the authorizations.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified application.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
ValueTask<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
ValueTask<ImmutableArray<TAuthorization>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
/// <summary>
/// Removes the specified authorization from the cache.
/// </summary>
/// <param name="authorization">The authorization to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task RemoveAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken);
}
}

84
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
{
/// <summary>
/// Provides methods allowing to cache scopes after retrieving them from the store.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
public interface IOpenIddictScopeCache<TScope> where TScope : class
{
/// <summary>
/// Add the specified scope to the cache.
/// </summary>
/// <param name="scope">The scope to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task AddAsync([NotNull] TScope scope, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
ValueTask<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
ValueTask<TScope> FindByNameAsync([NotNull] string name, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
ValueTask<ImmutableArray<TScope>> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken);
/// <summary>
/// Retrieves all the scopes that contain the specified resource.
/// </summary>
/// <param name="resource">The resource associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource.
/// </returns>
ValueTask<ImmutableArray<TScope>> FindByResourceAsync([NotNull] string resource, CancellationToken cancellationToken);
/// <summary>
/// Removes the specified scope from the cache.
/// </summary>
/// <param name="scope">The scope to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task RemoveAsync([NotNull] TScope scope, CancellationToken cancellationToken);
}
}

141
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
{
/// <summary>
/// Provides methods allowing to cache tokens after retrieving them from the store.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
public interface IOpenIddictTokenCache<TToken> where TToken : class
{
/// <summary>
/// Add the specified token to the cache.
/// </summary>
/// <param name="token">The token to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task AddAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the tokens corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the subject/client.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindAsync([NotNull] string subject,
[NotNull] string client, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="status">The token status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the criteria.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="status">The token status.</param>
/// <param name="type">The token type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the criteria.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, [NotNull] string type, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
ValueTask<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
ValueTask<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken);
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
ValueTask<ImmutableArray<TToken>> FindBySubjectAsync([NotNull] string subject, CancellationToken cancellationToken);
/// <summary>
/// Removes the specified token from the cache.
/// </summary>
/// <param name="token">The token to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task RemoveAsync([NotNull] TToken token, CancellationToken cancellationToken);
}
}

2
src/OpenIddict.Abstractions/Descriptors/OpenIddictTokenDescriptor.cs

@ -49,6 +49,8 @@ namespace OpenIddict.Abstractions
/// <summary> /// <summary>
/// Gets or sets the reference identifier associated with the token. /// 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.
/// </summary> /// </summary>
public string ReferenceId { get; set; } public string ReferenceId { get; set; }

13
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -318,7 +318,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
ValueTask<string> GetTokenTypeAsync([NotNull] object token, CancellationToken cancellationToken = default); ValueTask<string> GetTypeAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Determines whether a given token has already been redemeed. /// Determines whether a given token has already been redemeed.
@ -382,17 +382,6 @@ namespace OpenIddict.Abstractions
/// </returns> /// </returns>
Task<ImmutableArray<TResult>> ListAsync<TState, TResult>([NotNull] Func<IQueryable<object>, TState, IQueryable<TResult>> query, [CanBeNull] TState state, CancellationToken cancellationToken = default); Task<ImmutableArray<TResult>> ListAsync<TState, TResult>([NotNull] Func<IQueryable<object>, TState, IQueryable<TResult>> query, [CanBeNull] TState state, CancellationToken cancellationToken = default);
/// <summary>
/// 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.
/// </summary>
/// <param name="identifier">The client identifier.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
Task<string> ObfuscateReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default);
/// <summary> /// <summary>
/// Populates the specified descriptor using the properties exposed by the token. /// Populates the specified descriptor using the properties exposed by the token.
/// </summary> /// </summary>

3
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -272,15 +272,12 @@ namespace OpenIddict.Abstractions
public static class Properties public static class Properties
{ {
public const string Application = ".application";
public const string AuthenticationTicket = ".authentication_ticket"; public const string AuthenticationTicket = ".authentication_ticket";
public const string Error = ".error"; public const string Error = ".error";
public const string ErrorDescription = ".error_description"; public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri"; public const string ErrorUri = ".error_uri";
public const string InternalAuthorizationId = ".internal_authorization_id"; public const string InternalAuthorizationId = ".internal_authorization_id";
public const string InternalTokenId = ".internal_token_id"; public const string InternalTokenId = ".internal_token_id";
public const string ReferenceToken = ".reference_token";
public const string Token = ".token";
} }
public static class PropertyTypes public static class PropertyTypes

2
src/OpenIddict.Abstractions/Stores/IOpenIddictTokenStore.cs

@ -297,7 +297,7 @@ namespace OpenIddict.Abstractions
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken); ValueTask<string> GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Instantiates a new token. /// Instantiates a new token.

309
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
{
/// <summary>
/// Provides methods allowing to cache applications after retrieving them from the store.
/// </summary>
/// <typeparam name="TApplication">The type of the Application entity.</typeparam>
public class OpenIddictApplicationCache<TApplication> : IOpenIddictApplicationCache<TApplication>, IDisposable where TApplication : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictApplicationStore<TApplication> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
public OpenIddictApplicationCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
[NotNull] IOpenIddictApplicationStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_store = resolver.Get<TApplication>();
}
/// <summary>
/// Add the specified application to the cache.
/// </summary>
/// <param name="application">The application to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
/// <summary>
/// Retrieves an application using its client identifier.
/// </summary>
/// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public ValueTask<TApplication> 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<TApplication>(application);
}
async Task<TApplication> 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<TApplication>(ExecuteAsync());
}
/// <summary>
/// Retrieves an application using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier.
/// </returns>
public ValueTask<TApplication> 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<TApplication>(application);
}
async Task<TApplication> 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<TApplication>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the applications associated with the specified post_logout_redirect_uri.
/// </summary>
/// <param name="address">The post_logout_redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns>
public ValueTask<ImmutableArray<TApplication>> 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<TApplication> applications))
{
return new ValueTask<ImmutableArray<TApplication>>(applications);
}
async Task<ImmutableArray<TApplication>> 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<ImmutableArray<TApplication>>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the applications associated with the specified redirect_uri.
/// </summary>
/// <param name="address">The redirect_uri associated with the applications.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client applications corresponding to the specified redirect_uri.
/// </returns>
public ValueTask<ImmutableArray<TApplication>> 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<TApplication> applications))
{
return new ValueTask<ImmutableArray<TApplication>>(applications);
}
async Task<ImmutableArray<TApplication>> 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<ImmutableArray<TApplication>>(ExecuteAsync());
}
/// <summary>
/// Removes the specified application from the cache.
/// </summary>
/// <param name="application">The application to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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
});
}
}
}
}

505
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
{
/// <summary>
/// Provides methods allowing to cache authorizations after retrieving them from the store.
/// </summary>
/// <typeparam name="TAuthorization">The type of the Authorization entity.</typeparam>
public class OpenIddictAuthorizationCache<TAuthorization> : IOpenIddictAuthorizationCache<TAuthorization>, IDisposable where TAuthorization : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictAuthorizationStore<TAuthorization> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
public OpenIddictAuthorizationCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
[NotNull] IOpenIddictAuthorizationStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_store = resolver.Get<TAuthorization>();
}
/// <summary>
/// Add the specified authorization to the cache.
/// </summary>
/// <param name="authorization">The authorization to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
/// <summary>
/// Retrieves the authorizations corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the subject/client.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> 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<TAuthorization> authorizations))
{
return new ValueTask<ImmutableArray<TAuthorization>>(authorizations);
}
async Task<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> 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<TAuthorization> authorizations))
{
return new ValueTask<ImmutableArray<TAuthorization>>(authorizations);
}
async Task<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> 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<TAuthorization> authorizations))
{
return new ValueTask<ImmutableArray<TAuthorization>>(authorizations);
}
async Task<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the authorizations matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="client">The client associated with the authorization.</param>
/// <param name="status">The authorization status.</param>
/// <param name="type">The authorization type.</param>
/// <param name="scopes">The minimal scopes associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the criteria.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> FindAsync(
[NotNull] string subject, [NotNull] string client,
[NotNull] string status, [NotNull] string type,
ImmutableArray<string> 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<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the list of authorizations corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the authorizations.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified application.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> 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<TAuthorization> authorizations))
{
return new ValueTask<ImmutableArray<TAuthorization>>(authorizations);
}
async Task<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Retrieves an authorization using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier.
/// </returns>
public ValueTask<TAuthorization> 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<TAuthorization>(authorization);
}
async Task<TAuthorization> 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<TAuthorization>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the authorizations corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified subject.
/// </returns>
public ValueTask<ImmutableArray<TAuthorization>> 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<TAuthorization> authorizations))
{
return new ValueTask<ImmutableArray<TAuthorization>>(authorizations);
}
async Task<ImmutableArray<TAuthorization>> 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<ImmutableArray<TAuthorization>>(ExecuteAsync());
}
/// <summary>
/// Removes the specified authorization from the cache.
/// </summary>
/// <param name="authorization">The authorization to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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)
});
}
}
}

292
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
{
/// <summary>
/// Provides methods allowing to cache scopes after retrieving them from the store.
/// </summary>
/// <typeparam name="TScope">The type of the Scope entity.</typeparam>
public class OpenIddictScopeCache<TScope> : IOpenIddictScopeCache<TScope>, IDisposable where TScope : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictScopeStore<TScope> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
public OpenIddictScopeCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
[NotNull] IOpenIddictScopeStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_store = resolver.Get<TScope>();
}
/// <summary>
/// Add the specified scope to the cache.
/// </summary>
/// <param name="scope">The scope to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
/// <summary>
/// Retrieves a scope using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier.
/// </returns>
public ValueTask<TScope> 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<TScope>(scope);
}
async Task<TScope> 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<TScope>(ExecuteAsync());
}
/// <summary>
/// Retrieves a scope using its name.
/// </summary>
/// <param name="name">The name associated with the scope.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the specified name.
/// </returns>
public ValueTask<TScope> 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<TScope>(scope);
}
async Task<TScope> 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<TScope>(ExecuteAsync());
}
/// <summary>
/// Retrieves a list of scopes using their name.
/// </summary>
/// <param name="names">The names associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes corresponding to the specified names.
/// </returns>
public ValueTask<ImmutableArray<TScope>> FindByNamesAsync(ImmutableArray<string> names, CancellationToken cancellationToken)
{
if (names.IsDefaultOrEmpty)
{
return new ValueTask<ImmutableArray<TScope>>(ImmutableArray.Create<TScope>());
}
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<ImmutableArray<TScope>> ExecuteAsync()
{
var scopes = await _store.FindByNamesAsync(names, cancellationToken);
foreach (var scope in scopes)
{
await AddAsync(scope, cancellationToken);
}
return scopes;
}
return new ValueTask<ImmutableArray<TScope>>(ExecuteAsync());
}
/// <summary>
/// Retrieves all the scopes that contain the specified resource.
/// </summary>
/// <param name="resource">The resource associated with the scopes.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified resource.
/// </returns>
public ValueTask<ImmutableArray<TScope>> 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<TScope> scopes))
{
return new ValueTask<ImmutableArray<TScope>>(scopes);
}
async Task<ImmutableArray<TScope>> 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<ImmutableArray<TScope>>(ExecuteAsync());
}
/// <summary>
/// Removes the specified scope from the cache.
/// </summary>
/// <param name="scope">The scope to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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
});
}
}
}
}

562
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
{
/// <summary>
/// Provides methods allowing to cache tokens after retrieving them from the store.
/// </summary>
/// <typeparam name="TToken">The type of the Token entity.</typeparam>
public class OpenIddictTokenCache<TToken> : IOpenIddictTokenCache<TToken>, IDisposable where TToken : class
{
private readonly MemoryCache _cache;
private readonly IOpenIddictTokenStore<TToken> _store;
private readonly IOptions<OpenIddictCoreOptions> _options;
public OpenIddictTokenCache(
[NotNull] IOptions<OpenIddictCoreOptions> options,
[NotNull] IOpenIddictTokenStoreResolver resolver)
{
_cache = new MemoryCache(new MemoryCacheOptions());
_options = options;
_store = resolver.Get<TToken>();
}
/// <summary>
/// Add the specified token to the cache.
/// </summary>
/// <param name="token">The token to add to the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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);
}
}
/// <summary>
/// Disposes the cache held by this instance.
/// </summary>
public void Dispose() => _cache.Dispose();
/// <summary>
/// Retrieves the tokens corresponding to the specified
/// subject and associated with the application identifier.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the subject/client.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="status">The token status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the criteria.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the tokens matching the specified parameters.
/// </summary>
/// <param name="subject">The subject associated with the token.</param>
/// <param name="client">The client associated with the token.</param>
/// <param name="status">The token status.</param>
/// <param name="type">The token type.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the criteria.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified application identifier.
/// </summary>
/// <param name="identifier">The application identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified authorization identifier.
/// </summary>
/// <param name="identifier">The authorization identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Retrieves a token using its unique identifier.
/// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token corresponding to the unique identifier.
/// </returns>
public ValueTask<TToken> 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<TToken>(token);
}
async Task<TToken> 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<TToken>(ExecuteAsync());
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified reference identifier.
/// </returns>
public ValueTask<TToken> 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<TToken>(token);
}
async Task<TToken> 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<TToken>(ExecuteAsync());
}
/// <summary>
/// Retrieves the list of tokens corresponding to the specified subject.
/// </summary>
/// <param name="subject">The subject associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified subject.
/// </returns>
public ValueTask<ImmutableArray<TToken>> 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<TToken> tokens))
{
return new ValueTask<ImmutableArray<TToken>>(tokens);
}
async Task<ImmutableArray<TToken>> 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<ImmutableArray<TToken>>(ExecuteAsync());
}
/// <summary>
/// Removes the specified token from the cache.
/// </summary>
/// <param name="token">The token to remove from the cache.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
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)
});
}
}
}

126
src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs

@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplicationManager where TApplication : class public class OpenIddictApplicationManager<TApplication> : IOpenIddictApplicationManager where TApplication : class
{ {
public OpenIddictApplicationManager( public OpenIddictApplicationManager(
[NotNull] IOpenIddictApplicationCache<TApplication> cache,
[NotNull] IOpenIddictApplicationStoreResolver resolver, [NotNull] IOpenIddictApplicationStoreResolver resolver,
[NotNull] ILogger<OpenIddictApplicationManager<TApplication>> logger, [NotNull] ILogger<OpenIddictApplicationManager<TApplication>> logger,
[NotNull] IOptions<OpenIddictCoreOptions> options) [NotNull] IOptions<OpenIddictCoreOptions> options)
{ {
Cache = cache;
Store = resolver.Get<TApplication>(); Store = resolver.Get<TApplication>();
Logger = logger; Logger = logger;
Options = options; Options = options;
} }
/// <summary>
/// Gets the cache associated with the current manager.
/// </summary>
protected IOpenIddictApplicationCache<TApplication> Cache { get; }
/// <summary> /// <summary>
/// Gets the logger associated with the current manager. /// Gets the logger associated with the current manager.
/// </summary> /// </summary>
@ -115,7 +122,7 @@ namespace OpenIddict.Core
if (!string.IsNullOrEmpty(await Store.GetClientSecretAsync(application, cancellationToken))) 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. // 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); var application = await Store.InstantiateAsync(cancellationToken);
if (application == null) 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); await PopulateAsync(application, descriptor, cancellationToken);
@ -202,58 +209,89 @@ namespace OpenIddict.Core
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken = default)
{ {
if (application == null) if (application == null)
{ {
throw new ArgumentNullException(nameof(application)); throw new ArgumentNullException(nameof(application));
} }
return Store.DeleteAsync(application, cancellationToken); if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(application, cancellationToken);
}
await Store.DeleteAsync(application, cancellationToken);
} }
/// <summary> /// <summary>
/// Retrieves an application using its unique identifier. /// Retrieves an application using its client identifier.
/// </summary> /// </summary>
/// <param name="identifier">The unique identifier associated with the application.</param> /// <param name="identifier">The client identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
public virtual Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TApplication> FindByClientIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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;
} }
/// <summary> /// <summary>
/// Retrieves an application using its client identifier. /// Retrieves an application using its unique identifier.
/// </summary> /// </summary>
/// <param name="identifier">The client identifier associated with the application.</param> /// <param name="identifier">The unique identifier associated with the application.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client application corresponding to the identifier. /// whose result returns the client application corresponding to the identifier.
/// </returns> /// </returns>
public virtual async Task<TApplication> FindByClientIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TApplication> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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. // 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 // 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. // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
if (!Options.Value.DisableAdditionalFiltering &&
var application = await Store.FindByClientIdAsync(identifier, cancellationToken); !string.Equals(await Store.GetIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
if (application == null ||
!string.Equals(await Store.GetClientIdAsync(application, cancellationToken), identifier, StringComparison.Ordinal))
{ {
return null; return null;
} }
@ -278,16 +316,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address)); 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. var applications = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
var applications = await Store.FindByPostLogoutRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty) if (applications.IsEmpty)
{ {
return ImmutableArray.Create<TApplication>(); return ImmutableArray.Create<TApplication>();
} }
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<TApplication>(applications.Length); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Length);
foreach (var application in applications) foreach (var application in applications)
@ -324,16 +370,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The address cannot be null or empty.", nameof(address)); 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. var applications = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindByRedirectUriAsync(address, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindByRedirectUriAsync(address, cancellationToken);
var applications = await Store.FindByRedirectUriAsync(address, cancellationToken);
if (applications.IsEmpty) if (applications.IsEmpty)
{ {
return ImmutableArray.Create<TApplication>(); return ImmutableArray.Create<TApplication>();
} }
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<TApplication>(applications.Length); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Length);
foreach (var application in applications) foreach (var application in applications)
@ -366,6 +420,11 @@ namespace OpenIddict.Core
public virtual Task<TResult> GetAsync<TResult>( public virtual Task<TResult> GetAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return GetAsync((applications, state) => state(applications), query, cancellationToken); return GetAsync((applications, state) => state(applications), query, cancellationToken);
} }
@ -402,7 +461,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the client identifier associated with the application. /// whose result returns the client identifier associated with the application.
/// </returns> /// </returns>
public virtual ValueTask<string> GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetClientIdAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{ {
if (application == null) if (application == null)
{ {
@ -441,7 +501,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the consent type of the application (by default, "explicit"). /// whose result returns the consent type of the application (by default, "explicit").
/// </returns> /// </returns>
public virtual ValueTask<string> GetConsentTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetConsentTypeAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{ {
if (application == null) if (application == null)
{ {
@ -471,7 +532,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the display name associated with the application. /// whose result returns the display name associated with the application.
/// </returns> /// </returns>
public virtual ValueTask<string> GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetDisplayNameAsync(
[NotNull] TApplication application, CancellationToken cancellationToken = default)
{ {
if (application == null) if (application == null)
{ {
@ -679,6 +741,11 @@ namespace OpenIddict.Core
public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>( public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return ListAsync((applications, state) => state(applications), query, cancellationToken); return ListAsync((applications, state) => state(applications), query, cancellationToken);
} }
@ -838,6 +905,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
} }
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(application, cancellationToken);
}
await Store.UpdateAsync(application, cancellationToken); await Store.UpdateAsync(application, cancellationToken);
} }
@ -900,7 +972,7 @@ namespace OpenIddict.Core
var comparand = await Store.GetClientSecretAsync(application, cancellationToken); var comparand = await Store.GetClientSecretAsync(application, cancellationToken);
await PopulateAsync(application, descriptor, 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); var secret = await Store.GetClientSecretAsync(application, cancellationToken);
if (!string.Equals(secret, comparand, StringComparison.Ordinal)) if (!string.Equals(secret, comparand, StringComparison.Ordinal))
{ {

186
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthorizationManager where TAuthorization : class public class OpenIddictAuthorizationManager<TAuthorization> : IOpenIddictAuthorizationManager where TAuthorization : class
{ {
public OpenIddictAuthorizationManager( public OpenIddictAuthorizationManager(
[NotNull] IOpenIddictAuthorizationCache<TAuthorization> cache,
[NotNull] IOpenIddictAuthorizationStoreResolver resolver, [NotNull] IOpenIddictAuthorizationStoreResolver resolver,
[NotNull] ILogger<OpenIddictAuthorizationManager<TAuthorization>> logger, [NotNull] ILogger<OpenIddictAuthorizationManager<TAuthorization>> logger,
[NotNull] IOptions<OpenIddictCoreOptions> options) [NotNull] IOptions<OpenIddictCoreOptions> options)
{ {
Cache = cache;
Store = resolver.Get<TAuthorization>(); Store = resolver.Get<TAuthorization>();
Logger = logger; Logger = logger;
Options = options; Options = options;
} }
/// <summary>
/// Gets the cache associated with the current manager.
/// </summary>
protected IOpenIddictAuthorizationCache<TAuthorization> Cache { get; }
/// <summary> /// <summary>
/// Gets the logger associated with the current manager. /// Gets the logger associated with the current manager.
/// </summary> /// </summary>
@ -217,14 +224,19 @@ namespace OpenIddict.Core
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public virtual Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual async Task DeleteAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
throw new ArgumentNullException(nameof(authorization)); throw new ArgumentNullException(nameof(authorization));
} }
return Store.DeleteAsync(authorization, cancellationToken); if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(authorization, cancellationToken);
}
await Store.DeleteAsync(authorization, cancellationToken);
} }
/// <summary> /// <summary>
@ -251,14 +263,22 @@ namespace OpenIddict.Core
throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); 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<TAuthorization>();
}
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. // 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 // 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. // 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 (Options.Value.DisableAdditionalFiltering)
if (authorizations.IsEmpty)
{ {
return ImmutableArray.Create<TAuthorization>(); return authorizations;
} }
var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length); var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
@ -306,16 +326,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The status cannot be null or empty.", nameof(status)); throw new ArgumentException("The status cannot be null or empty.", nameof(status));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var authorizations = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, status, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, status, cancellationToken);
var authorizations = await Store.FindAsync(subject, client, status, cancellationToken);
if (authorizations.IsEmpty) if (authorizations.IsEmpty)
{ {
return ImmutableArray.Create<TAuthorization>(); return ImmutableArray.Create<TAuthorization>();
} }
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<TAuthorization>(authorizations.Length); var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations) foreach (var authorization in authorizations)
@ -367,16 +395,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type)); throw new ArgumentException("The type cannot be null or empty.", nameof(type));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var authorizations = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, status, type, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, status, type, cancellationToken);
var authorizations = await Store.FindAsync(subject, client, status, type, cancellationToken);
if (authorizations.IsEmpty) if (authorizations.IsEmpty)
{ {
return ImmutableArray.Create<TAuthorization>(); return ImmutableArray.Create<TAuthorization>();
} }
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<TAuthorization>(authorizations.Length); var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations) foreach (var authorization in authorizations)
@ -430,16 +466,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type)); throw new ArgumentException("The type cannot be null or empty.", nameof(type));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var authorizations = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, status, type, scopes, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, status, type, scopes, cancellationToken);
var authorizations = await Store.FindAsync(subject, client, status, type, scopes, cancellationToken);
if (authorizations.IsEmpty) if (authorizations.IsEmpty)
{ {
return ImmutableArray.Create<TAuthorization>(); return ImmutableArray.Create<TAuthorization>();
} }
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<TAuthorization>(authorizations.Length); var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations) foreach (var authorization in authorizations)
@ -465,7 +509,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorizations corresponding to the specified application. /// whose result returns the authorizations corresponding to the specified application.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TAuthorization>> FindByApplicationIdAsync( public virtual async Task<ImmutableArray<TAuthorization>> FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken = default) [NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
@ -473,7 +517,37 @@ namespace OpenIddict.Core
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); 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<TAuthorization>();
}
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<TAuthorization>(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();
} }
/// <summary> /// <summary>
@ -485,14 +559,32 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the authorization corresponding to the identifier. /// whose result returns the authorization corresponding to the identifier.
/// </returns> /// </returns>
public virtual Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TAuthorization> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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;
} }
/// <summary> /// <summary>
@ -512,16 +604,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); 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. var authorizations = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindBySubjectAsync(subject, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindBySubjectAsync(subject, cancellationToken);
var authorizations = await Store.FindBySubjectAsync(subject, cancellationToken);
if (authorizations.IsEmpty) if (authorizations.IsEmpty)
{ {
return ImmutableArray.Create<TAuthorization>(); return ImmutableArray.Create<TAuthorization>();
} }
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<TAuthorization>(authorizations.Length); var builder = ImmutableArray.CreateBuilder<TAuthorization>(authorizations.Length);
foreach (var authorization in authorizations) foreach (var authorization in authorizations)
@ -570,6 +670,11 @@ namespace OpenIddict.Core
public virtual Task<TResult> GetAsync<TResult>( public virtual Task<TResult> GetAsync<TResult>(
[NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return GetAsync((authorizations, state) => state(authorizations), query, cancellationToken); return GetAsync((authorizations, state) => state(authorizations), query, cancellationToken);
} }
@ -625,7 +730,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization. /// whose result returns the scopes associated with the specified authorization.
/// </returns> /// </returns>
public virtual ValueTask<ImmutableArray<string>> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual ValueTask<ImmutableArray<string>> GetScopesAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -644,7 +750,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the status associated with the specified authorization. /// whose result returns the status associated with the specified authorization.
/// </returns> /// </returns>
public virtual ValueTask<string> GetStatusAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetStatusAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -663,7 +770,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the subject associated with the specified authorization. /// whose result returns the subject associated with the specified authorization.
/// </returns> /// </returns>
public virtual ValueTask<string> GetSubjectAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetSubjectAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -682,7 +790,8 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the type associated with the specified authorization. /// whose result returns the type associated with the specified authorization.
/// </returns> /// </returns>
public virtual ValueTask<string> GetTypeAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetTypeAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -740,7 +849,8 @@ namespace OpenIddict.Core
/// <param name="authorization">The authorization.</param> /// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is permanent, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the authorization is permanent, <c>false</c> otherwise.</returns>
public async Task<bool> IsPermanentAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public async Task<bool> IsPermanentAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -762,7 +872,8 @@ namespace OpenIddict.Core
/// <param name="authorization">The authorization.</param> /// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization has been revoked, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the authorization has been revoked, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsRevokedAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual async Task<bool> IsRevokedAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -784,7 +895,8 @@ namespace OpenIddict.Core
/// <param name="authorization">The authorization.</param> /// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is valid, <c>false</c> otherwise.</returns> /// <returns><c>true</c> if the authorization is valid, <c>false</c> otherwise.</returns>
public virtual async Task<bool> IsValidAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) public virtual async Task<bool> IsValidAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{ {
if (authorization == null) if (authorization == null)
{ {
@ -829,6 +941,11 @@ namespace OpenIddict.Core
public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>( public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>(
[NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return ListAsync((authorizations, state) => state(authorizations), query, cancellationToken); return ListAsync((authorizations, state) => state(authorizations), query, cancellationToken);
} }
@ -998,6 +1115,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
} }
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(authorization, cancellationToken);
}
await Store.UpdateAsync(authorization, cancellationToken); await Store.UpdateAsync(authorization, cancellationToken);
} }

98
src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs

@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TScope : class public class OpenIddictScopeManager<TScope> : IOpenIddictScopeManager where TScope : class
{ {
public OpenIddictScopeManager( public OpenIddictScopeManager(
[NotNull] IOpenIddictScopeCache<TScope> cache,
[NotNull] IOpenIddictScopeStoreResolver resolver, [NotNull] IOpenIddictScopeStoreResolver resolver,
[NotNull] ILogger<OpenIddictScopeManager<TScope>> logger, [NotNull] ILogger<OpenIddictScopeManager<TScope>> logger,
[NotNull] IOptions<OpenIddictCoreOptions> options) [NotNull] IOptions<OpenIddictCoreOptions> options)
{ {
Cache = cache;
Store = resolver.Get<TScope>(); Store = resolver.Get<TScope>();
Logger = logger; Logger = logger;
Options = options; Options = options;
} }
/// <summary>
/// Gets the cache associated with the current manager.
/// </summary>
protected IOpenIddictScopeCache<TScope> Cache { get; }
/// <summary> /// <summary>
/// Gets the logger associated with the current manager. /// Gets the logger associated with the current manager.
/// </summary> /// </summary>
@ -151,14 +158,19 @@ namespace OpenIddict.Core
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public virtual Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken = default) public virtual async Task DeleteAsync([NotNull] TScope scope, CancellationToken cancellationToken = default)
{ {
if (scope == null) if (scope == null)
{ {
throw new ArgumentNullException(nameof(scope)); throw new ArgumentNullException(nameof(scope));
} }
return Store.DeleteAsync(scope, cancellationToken); if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(scope, cancellationToken);
}
await Store.DeleteAsync(scope, cancellationToken);
} }
/// <summary> /// <summary>
@ -170,14 +182,32 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scope corresponding to the identifier. /// whose result returns the scope corresponding to the identifier.
/// </returns> /// </returns>
public virtual Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TScope> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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;
} }
/// <summary> /// <summary>
@ -196,12 +226,21 @@ namespace OpenIddict.Core
throw new ArgumentException("The scope name cannot be null or empty.", nameof(name)); 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. // 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 // 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. // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
var scope = await Store.FindByNameAsync(name, cancellationToken); if (!Options.Value.DisableAdditionalFiltering &&
if (scope == null || !string.Equals(await Store.GetNameAsync(scope, cancellationToken), name, StringComparison.Ordinal)) !string.Equals(await Store.GetNameAsync(scope, cancellationToken), name, StringComparison.Ordinal))
{ {
return null; return null;
} }
@ -231,16 +270,24 @@ namespace OpenIddict.Core
throw new ArgumentException("Scope names cannot be null or empty.", nameof(names)); 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. var scopes = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindByNamesAsync(names, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindByNamesAsync(names, cancellationToken);
var scopes = await Store.FindByNamesAsync(names, cancellationToken);
if (scopes.IsEmpty) if (scopes.IsEmpty)
{ {
return ImmutableArray.Create<TScope>(); return ImmutableArray.Create<TScope>();
} }
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<TScope>(scopes.Length); var builder = ImmutableArray.CreateBuilder<TScope>(scopes.Length);
foreach (var scope in scopes) foreach (var scope in scopes)
@ -273,16 +320,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The resource cannot be null or empty.", nameof(resource)); 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. var scopes = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindByResourceAsync(resource, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindByResourceAsync(resource, cancellationToken);
var scopes = await Store.FindByResourceAsync(resource, cancellationToken);
if (scopes.IsEmpty) if (scopes.IsEmpty)
{ {
return ImmutableArray.Create<TScope>(); return ImmutableArray.Create<TScope>();
} }
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<TScope>(scopes.Length); var builder = ImmutableArray.CreateBuilder<TScope>(scopes.Length);
foreach (var scope in scopes) foreach (var scope in scopes)
@ -314,6 +369,11 @@ namespace OpenIddict.Core
public virtual Task<TResult> GetAsync<TResult>( public virtual Task<TResult> GetAsync<TResult>(
[NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return GetAsync((scopes, state) => state(scopes), query, cancellationToken); return GetAsync((scopes, state) => state(scopes), query, cancellationToken);
} }
@ -466,6 +526,11 @@ namespace OpenIddict.Core
public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>( public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>(
[NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return ListAsync((scopes, state) => state(scopes), query, cancellationToken); return ListAsync((scopes, state) => state(scopes), query, cancellationToken);
} }
@ -614,6 +679,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
} }
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(scope, cancellationToken);
}
await Store.UpdateAsync(scope, cancellationToken); await Store.UpdateAsync(scope, cancellationToken);
} }

303
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -26,15 +26,22 @@ namespace OpenIddict.Core
public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TToken : class public class OpenIddictTokenManager<TToken> : IOpenIddictTokenManager where TToken : class
{ {
public OpenIddictTokenManager( public OpenIddictTokenManager(
[NotNull] IOpenIddictTokenCache<TToken> cache,
[NotNull] IOpenIddictTokenStoreResolver resolver, [NotNull] IOpenIddictTokenStoreResolver resolver,
[NotNull] ILogger<OpenIddictTokenManager<TToken>> logger, [NotNull] ILogger<OpenIddictTokenManager<TToken>> logger,
[NotNull] IOptions<OpenIddictCoreOptions> options) [NotNull] IOptions<OpenIddictCoreOptions> options)
{ {
Cache = cache;
Store = resolver.Get<TToken>(); Store = resolver.Get<TToken>();
Logger = logger; Logger = logger;
Options = options; Options = options;
} }
/// <summary>
/// Gets the cache associated with the current manager.
/// </summary>
protected IOpenIddictTokenCache<TToken> Cache { get; }
/// <summary> /// <summary>
/// Gets the logger associated with the current manager. /// Gets the logger associated with the current manager.
/// </summary> /// </summary>
@ -103,6 +110,14 @@ namespace OpenIddict.Core
await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Valid, cancellationToken); 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); var results = await ValidateAsync(token, cancellationToken);
if (results.Any(result => result != ValidationResult.Success)) if (results.Any(result => result != ValidationResult.Success))
{ {
@ -157,14 +172,19 @@ namespace OpenIddict.Core
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation. /// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns> /// </returns>
public virtual Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken = default) public virtual async Task DeleteAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
{ {
if (token == null) if (token == null)
{ {
throw new ArgumentNullException(nameof(token)); throw new ArgumentNullException(nameof(token));
} }
return Store.DeleteAsync(token, cancellationToken); if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(token, cancellationToken);
}
await Store.DeleteAsync(token, cancellationToken);
} }
/// <summary> /// <summary>
@ -212,16 +232,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client)); throw new ArgumentException("The client identifier cannot be null or empty.", nameof(client));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var tokens = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, cancellationToken);
var tokens = await Store.FindAsync(subject, client, cancellationToken);
if (tokens.IsEmpty) if (tokens.IsEmpty)
{ {
return ImmutableArray.Create<TToken>(); return ImmutableArray.Create<TToken>();
} }
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<TToken>(tokens.Length); var builder = ImmutableArray.CreateBuilder<TToken>(tokens.Length);
foreach (var token in tokens) foreach (var token in tokens)
@ -267,16 +295,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The status cannot be null or empty.", nameof(status)); throw new ArgumentException("The status cannot be null or empty.", nameof(status));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var tokens = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, status, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, status, cancellationToken);
var tokens = await Store.FindAsync(subject, client, status, cancellationToken);
if (tokens.IsEmpty) if (tokens.IsEmpty)
{ {
return ImmutableArray.Create<TToken>(); return ImmutableArray.Create<TToken>();
} }
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<TToken>(tokens.Length); var builder = ImmutableArray.CreateBuilder<TToken>(tokens.Length);
foreach (var token in tokens) foreach (var token in tokens)
@ -328,16 +364,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The type cannot be null or empty.", nameof(type)); throw new ArgumentException("The type cannot be null or empty.", nameof(type));
} }
// SQL engines like Microsoft SQL Server or MySQL are known to use case-insensitive lookups by default. var tokens = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindAsync(subject, client, status, type, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindAsync(subject, client, status, type, cancellationToken);
var tokens = await Store.FindAsync(subject, client, status, type, cancellationToken);
if (tokens.IsEmpty) if (tokens.IsEmpty)
{ {
return ImmutableArray.Create<TToken>(); return ImmutableArray.Create<TToken>();
} }
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<TToken>(tokens.Length); var builder = ImmutableArray.CreateBuilder<TToken>(tokens.Length);
foreach (var token in tokens) foreach (var token in tokens)
@ -362,7 +406,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application. /// whose result returns the tokens corresponding to the specified application.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TToken>> FindByApplicationIdAsync( public virtual async Task<ImmutableArray<TToken>> FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken = default) [NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
@ -370,7 +414,37 @@ namespace OpenIddict.Core
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); 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<TToken>();
}
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<TToken>(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();
} }
/// <summary> /// <summary>
@ -382,7 +456,7 @@ namespace OpenIddict.Core
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization. /// whose result returns the tokens corresponding to the specified authorization.
/// </returns> /// </returns>
public virtual Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync( public virtual async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken = default) [NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
@ -390,35 +464,69 @@ namespace OpenIddict.Core
throw new ArgumentException("The identifier cannot be null or empty.", nameof(identifier)); 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<TToken>();
}
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<TToken>(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();
} }
/// <summary> /// <summary>
/// Retrieves the list of tokens corresponding to the specified reference identifier. /// Retrieves a token using its unique identifier.
/// Note: the reference identifier may be hashed or encrypted for security reasons.
/// </summary> /// </summary>
/// <param name="identifier">The reference identifier associated with the tokens.</param> /// <param name="identifier">The unique identifier associated with the token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> 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.
/// </returns> /// </returns>
public virtual async Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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. // 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 // 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. // used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here.
if (!Options.Value.DisableAdditionalFiltering &&
identifier = await ObfuscateReferenceIdAsync(identifier, cancellationToken); !string.Equals(await Store.GetIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal))
var token = await Store.FindByReferenceIdAsync(identifier, cancellationToken);
if (token == null ||
!string.Equals(await Store.GetReferenceIdAsync(token, cancellationToken), identifier, StringComparison.Ordinal))
{ {
return null; return null;
} }
@ -427,22 +535,44 @@ namespace OpenIddict.Core
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <param name="identifier">The unique identifier associated with the token.</param> /// <param name="identifier">The reference identifier associated with the tokens.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns> /// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> 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.
/// </returns> /// </returns>
public virtual Task<TToken> FindByIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default) public virtual async Task<TToken> FindByReferenceIdAsync([NotNull] string identifier, CancellationToken cancellationToken = default)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
throw new ArgumentException("The identifier cannot be null or empty.", nameof(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;
} }
/// <summary> /// <summary>
@ -462,16 +592,24 @@ namespace OpenIddict.Core
throw new ArgumentException("The subject cannot be null or empty.", nameof(subject)); 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. var tokens = Options.Value.DisableEntityCaching ?
// To ensure a case-sensitive comparison is enforced independently of the database/table/query collation await Store.FindBySubjectAsync(subject, cancellationToken) :
// used by the store, a second pass using string.Equals(StringComparison.Ordinal) is manually made here. await Cache.FindBySubjectAsync(subject, cancellationToken);
var tokens = await Store.FindBySubjectAsync(subject, cancellationToken);
if (tokens.IsEmpty) if (tokens.IsEmpty)
{ {
return ImmutableArray.Create<TToken>(); return ImmutableArray.Create<TToken>();
} }
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<TToken>(tokens.Length); var builder = ImmutableArray.CreateBuilder<TToken>(tokens.Length);
foreach (var token in tokens) foreach (var token in tokens)
@ -519,6 +657,11 @@ namespace OpenIddict.Core
public virtual Task<TResult> GetAsync<TResult>( public virtual Task<TResult> GetAsync<TResult>(
[NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return GetAsync((tokens, state) => state(tokens), query, cancellationToken); return GetAsync((tokens, state) => state(tokens), query, cancellationToken);
} }
@ -709,14 +852,14 @@ namespace OpenIddict.Core
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken = default) public virtual ValueTask<string> GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
{ {
if (token == null) if (token == null)
{ {
throw new ArgumentNullException(nameof(token)); throw new ArgumentNullException(nameof(token));
} }
return Store.GetTokenTypeAsync(token, cancellationToken); return Store.GetTypeAsync(token, cancellationToken);
} }
/// <summary> /// <summary>
@ -814,6 +957,11 @@ namespace OpenIddict.Core
public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>( public virtual Task<ImmutableArray<TResult>> ListAsync<TResult>(
[NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken = default) [NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken = default)
{ {
if (query == null)
{
throw new ArgumentNullException(nameof(query));
}
return ListAsync((tokens, state) => state(tokens), query, cancellationToken); return ListAsync((tokens, state) => state(tokens), query, cancellationToken);
} }
@ -841,30 +989,6 @@ namespace OpenIddict.Core
return Store.ListAsync(query, state, cancellationToken); return Store.ListAsync(query, state, cancellationToken);
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="identifier">The client identifier.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
public virtual Task<string> 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))));
}
}
/// <summary> /// <summary>
/// Populates the token using the specified descriptor. /// Populates the token using the specified descriptor.
/// </summary> /// </summary>
@ -929,7 +1053,7 @@ namespace OpenIddict.Core
descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken); descriptor.ReferenceId = await Store.GetReferenceIdAsync(token, cancellationToken);
descriptor.Status = await Store.GetStatusAsync(token, cancellationToken); descriptor.Status = await Store.GetStatusAsync(token, cancellationToken);
descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken); descriptor.Subject = await Store.GetSubjectAsync(token, cancellationToken);
descriptor.Type = await Store.GetTokenTypeAsync(token, cancellationToken); descriptor.Type = await Store.GetTypeAsync(token, cancellationToken);
} }
/// <summary> /// <summary>
@ -1056,6 +1180,11 @@ namespace OpenIddict.Core
throw new OpenIddictExceptions.ValidationException(builder.ToString(), results); throw new OpenIddictExceptions.ValidationException(builder.ToString(), results);
} }
if (!Options.Value.DisableEntityCaching)
{
await Cache.RemoveAsync(token, cancellationToken);
}
await Store.UpdateAsync(token, cancellationToken); await Store.UpdateAsync(token, cancellationToken);
} }
@ -1081,7 +1210,18 @@ namespace OpenIddict.Core
throw new ArgumentNullException(nameof(descriptor)); 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); 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); 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)) if (string.IsNullOrEmpty(type))
{ {
builder.Add(new ValidationResult("The token type cannot be null or empty.")); builder.Add(new ValidationResult("The token type cannot be null or empty."));
@ -1150,6 +1290,30 @@ namespace OpenIddict.Core
builder.ToImmutable(); builder.ToImmutable();
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="identifier">The client identifier.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.
/// </returns>
protected virtual Task<string> 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<long> IOpenIddictTokenManager.CountAsync(CancellationToken cancellationToken) Task<long> IOpenIddictTokenManager.CountAsync(CancellationToken cancellationToken)
=> CountAsync(cancellationToken); => CountAsync(cancellationToken);
@ -1225,8 +1389,8 @@ namespace OpenIddict.Core
ValueTask<string> IOpenIddictTokenManager.GetSubjectAsync(object token, CancellationToken cancellationToken) ValueTask<string> IOpenIddictTokenManager.GetSubjectAsync(object token, CancellationToken cancellationToken)
=> GetSubjectAsync((TToken) token, cancellationToken); => GetSubjectAsync((TToken) token, cancellationToken);
ValueTask<string> IOpenIddictTokenManager.GetTokenTypeAsync(object token, CancellationToken cancellationToken) ValueTask<string> IOpenIddictTokenManager.GetTypeAsync(object token, CancellationToken cancellationToken)
=> GetTokenTypeAsync((TToken) token, cancellationToken); => GetTypeAsync((TToken) token, cancellationToken);
Task<bool> IOpenIddictTokenManager.IsRedeemedAsync(object token, CancellationToken cancellationToken) Task<bool> IOpenIddictTokenManager.IsRedeemedAsync(object token, CancellationToken cancellationToken)
=> IsRedeemedAsync((TToken) token, cancellationToken); => IsRedeemedAsync((TToken) token, cancellationToken);
@ -1246,9 +1410,6 @@ namespace OpenIddict.Core
Task<ImmutableArray<TResult>> IOpenIddictTokenManager.ListAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken) Task<ImmutableArray<TResult>> IOpenIddictTokenManager.ListAsync<TState, TResult>(Func<IQueryable<object>, TState, IQueryable<TResult>> query, TState state, CancellationToken cancellationToken)
=> ListAsync(query, state, cancellationToken); => ListAsync(query, state, cancellationToken);
Task<string> IOpenIddictTokenManager.ObfuscateReferenceIdAsync(string identifier, CancellationToken cancellationToken)
=> ObfuscateReferenceIdAsync(identifier, cancellationToken);
Task IOpenIddictTokenManager.PopulateAsync(OpenIddictTokenDescriptor descriptor, object token, CancellationToken cancellationToken) Task IOpenIddictTokenManager.PopulateAsync(OpenIddictTokenDescriptor descriptor, object token, CancellationToken cancellationToken)
=> PopulateAsync(descriptor, (TToken) token, cancellationToken); => PopulateAsync(descriptor, (TToken) token, cancellationToken);

1
src/OpenIddict.Core/OpenIddict.Core.csproj

@ -19,6 +19,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="CryptoHelper" Version="$(CryptoHelperVersion)" /> <PackageReference Include="CryptoHelper" Version="$(CryptoHelperVersion)" />
<PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" /> <PackageReference Include="JetBrains.Annotations" Version="$(JetBrainsVersion)" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" /> <PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
</ItemGroup> </ItemGroup>

111
src/OpenIddict.Core/OpenIddictCoreBuilder.cs

@ -293,11 +293,10 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped) public OpenIddictCoreBuilder ReplaceApplicationManager<TManager>()
where TManager : class where TManager : class
=> ReplaceApplicationManager(typeof(TManager), lifetime); => ReplaceApplicationManager(typeof(TManager));
/// <summary> /// <summary>
/// Replace the default application manager by a custom manager derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceApplicationManager( public OpenIddictCoreBuilder ReplaceApplicationManager([NotNull] Type type)
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -331,8 +328,8 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); throw new ArgumentException("The specified type is invalid.", nameof(type));
} }
Services.Replace(new ServiceDescriptor(type, type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, type));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>), type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>), type));
} }
else else
@ -341,9 +338,9 @@ namespace Microsoft.Extensions.DependencyInjection
=> provider.GetRequiredService(typeof(OpenIddictApplicationManager<>) => provider.GetRequiredService(typeof(OpenIddictApplicationManager<>)
.MakeGenericType(root.GenericTypeArguments[0])); .MakeGenericType(root.GenericTypeArguments[0]));
Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictApplicationManager<>) Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictApplicationManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); .MakeGenericType(root.GenericTypeArguments[0]), type));
} }
return this; return this;
@ -390,11 +387,10 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped) public OpenIddictCoreBuilder ReplaceAuthorizationManager<TManager>()
where TManager : class where TManager : class
=> ReplaceAuthorizationManager(typeof(TManager), lifetime); => ReplaceAuthorizationManager(typeof(TManager));
/// <summary> /// <summary>
/// Replace the default authorization manager by a custom manager derived /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceAuthorizationManager( public OpenIddictCoreBuilder ReplaceAuthorizationManager([NotNull] Type type)
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -428,8 +422,8 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); throw new ArgumentException("The specified type is invalid.", nameof(type));
} }
Services.Replace(new ServiceDescriptor(type, type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, type));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>), type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>), type));
} }
else else
@ -438,9 +432,9 @@ namespace Microsoft.Extensions.DependencyInjection
=> provider.GetRequiredService(typeof(OpenIddictAuthorizationManager<>) => provider.GetRequiredService(typeof(OpenIddictAuthorizationManager<>)
.MakeGenericType(root.GenericTypeArguments[0])); .MakeGenericType(root.GenericTypeArguments[0]));
Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictAuthorizationManager<>) Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictAuthorizationManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); .MakeGenericType(root.GenericTypeArguments[0]), type));
} }
return this; return this;
@ -488,9 +482,9 @@ namespace Microsoft.Extensions.DependencyInjection
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped) public OpenIddictCoreBuilder ReplaceScopeManager<TManager>()
where TManager : class where TManager : class
=> ReplaceScopeManager(typeof(TManager), lifetime); => ReplaceScopeManager(typeof(TManager));
/// <summary> /// <summary>
/// Replace the default scope manager by a custom manager /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceScopeManager( public OpenIddictCoreBuilder ReplaceScopeManager([NotNull] Type type)
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -524,8 +516,8 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); throw new ArgumentException("The specified type is invalid.", nameof(type));
} }
Services.Replace(new ServiceDescriptor(type, type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, type));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>), type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>), type));
} }
else else
@ -534,9 +526,9 @@ namespace Microsoft.Extensions.DependencyInjection
=> provider.GetRequiredService(typeof(OpenIddictScopeManager<>) => provider.GetRequiredService(typeof(OpenIddictScopeManager<>)
.MakeGenericType(root.GenericTypeArguments[0])); .MakeGenericType(root.GenericTypeArguments[0]));
Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictScopeManager<>) Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictScopeManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); .MakeGenericType(root.GenericTypeArguments[0]), type));
} }
return this; return this;
@ -583,11 +575,10 @@ namespace Microsoft.Extensions.DependencyInjection
/// must be either a non-generic or closed generic service. /// must be either a non-generic or closed generic service.
/// </summary> /// </summary>
/// <typeparam name="TManager">The type of the custom manager.</typeparam> /// <typeparam name="TManager">The type of the custom manager.</typeparam>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenManager<TManager>(ServiceLifetime lifetime = ServiceLifetime.Scoped) public OpenIddictCoreBuilder ReplaceTokenManager<TManager>()
where TManager : class where TManager : class
=> ReplaceTokenManager(typeof(TManager), lifetime); => ReplaceTokenManager(typeof(TManager));
/// <summary> /// <summary>
/// Replace the default token manager by a custom manager /// 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. /// either a non-generic, a closed or an open generic service.
/// </summary> /// </summary>
/// <param name="type">The type of the custom manager.</param> /// <param name="type">The type of the custom manager.</param>
/// <param name="lifetime">The lifetime of the registered service.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns> /// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder ReplaceTokenManager( public OpenIddictCoreBuilder ReplaceTokenManager([NotNull] Type type)
[NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped)
{ {
if (type == null) if (type == null)
{ {
@ -621,8 +610,8 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentException("The specified type is invalid.", nameof(type)); throw new ArgumentException("The specified type is invalid.", nameof(type));
} }
Services.Replace(new ServiceDescriptor(type, type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, type));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>), type, lifetime)); Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>), type));
} }
else else
@ -631,9 +620,9 @@ namespace Microsoft.Extensions.DependencyInjection
=> provider.GetRequiredService(typeof(OpenIddictTokenManager<>) => provider.GetRequiredService(typeof(OpenIddictTokenManager<>)
.MakeGenericType(root.GenericTypeArguments[0])); .MakeGenericType(root.GenericTypeArguments[0]));
Services.Replace(new ServiceDescriptor(type, ResolveManager, lifetime)); Services.Replace(ServiceDescriptor.Scoped(type, ResolveManager));
Services.Replace(new ServiceDescriptor(typeof(OpenIddictTokenManager<>) Services.Replace(ServiceDescriptor.Scoped(typeof(OpenIddictTokenManager<>)
.MakeGenericType(root.GenericTypeArguments[0]), type, lifetime)); .MakeGenericType(root.GenericTypeArguments[0]), type));
} }
return this; return this;
@ -673,6 +662,26 @@ namespace Microsoft.Extensions.DependencyInjection
return this; return this;
} }
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder DisableAdditionalFiltering()
=> Configure(options => options.DisableAdditionalFiltering = true);
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
public OpenIddictCoreBuilder DisableEntityCaching()
=> Configure(options => options.DisableEntityCaching = true);
/// <summary> /// <summary>
/// Configures OpenIddict to use the specified entity as the default application entity. /// Configures OpenIddict to use the specified entity as the default application entity.
/// </summary> /// </summary>
@ -780,5 +789,21 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.DefaultTokenType = type); return Configure(options => options.DefaultTokenType = type);
} }
/// <summary>
/// Configures OpenIddict to use the specified entity cache limit,
/// after which the internal cache is automatically compacted.
/// </summary>
/// <param name="limit">The cache limit, in number of entries.</param>
/// <returns>The <see cref="OpenIddictCoreBuilder"/>.</returns>
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);
}
} }
} }

5
src/OpenIddict.Core/OpenIddictCoreExtensions.cs

@ -40,6 +40,11 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddScoped(typeof(OpenIddictScopeManager<>)); builder.Services.TryAddScoped(typeof(OpenIddictScopeManager<>));
builder.Services.TryAddScoped(typeof(OpenIddictTokenManager<>)); 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<IOpenIddictApplicationStoreResolver, OpenIddictApplicationStoreResolver>(); builder.Services.TryAddScoped<IOpenIddictApplicationStoreResolver, OpenIddictApplicationStoreResolver>();
builder.Services.TryAddScoped<IOpenIddictAuthorizationStoreResolver, OpenIddictAuthorizationStoreResolver>(); builder.Services.TryAddScoped<IOpenIddictAuthorizationStoreResolver, OpenIddictAuthorizationStoreResolver>();
builder.Services.TryAddScoped<IOpenIddictScopeStoreResolver, OpenIddictScopeStoreResolver>(); builder.Services.TryAddScoped<IOpenIddictScopeStoreResolver, OpenIddictScopeStoreResolver>();

25
src/OpenIddict.Core/OpenIddictCoreOptions.cs

@ -36,5 +36,30 @@ namespace OpenIddict.Core
/// used by the non-generic token manager and the server/validation services. /// used by the non-generic token manager and the server/validation services.
/// </summary> /// </summary>
public Type DefaultTokenType { get; set; } public Type DefaultTokenType { get; set; }
/// <summary>
/// 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 <c>true</c> 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.
/// </summary>
public bool DisableAdditionalFiltering { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool DisableEntityCaching { get; set; }
/// <summary>
/// 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 <see cref="DisableEntityCaching"/> is <c>true</c>.
/// </summary>
public int EntityCacheLimit { get; set; } = 250;
} }
} }

4
src/OpenIddict.Core/Resolvers/OpenIddictApplicationStoreResolver.cs

@ -14,9 +14,7 @@ namespace OpenIddict.Core
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns an application store compatible with the specified application type or throws an /// Returns an application store compatible with the specified application type or throws an

4
src/OpenIddict.Core/Resolvers/OpenIddictAuthorizationStoreResolver.cs

@ -14,9 +14,7 @@ namespace OpenIddict.Core
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns an authorization store compatible with the specified authorization type or throws an /// Returns an authorization store compatible with the specified authorization type or throws an

4
src/OpenIddict.Core/Resolvers/OpenIddictScopeStoreResolver.cs

@ -14,9 +14,7 @@ namespace OpenIddict.Core
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns a scope store compatible with the specified scope type or throws an /// Returns a scope store compatible with the specified scope type or throws an

4
src/OpenIddict.Core/Resolvers/OpenIddictTokenStoreResolver.cs

@ -14,9 +14,7 @@ namespace OpenIddict.Core
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns a token store compatible with the specified token type or throws an /// Returns a token store compatible with the specified token type or throws an

5
src/OpenIddict.EntityFramework/OpenIddictEntityFrameworkExtensions.cs

@ -34,6 +34,11 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddMemoryCache(); 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<OpenIddictApplication>() builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() .SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>() .SetDefaultScopeEntity<OpenIddictScope>()

25
src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs

@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -282,7 +282,8 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri. /// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -298,7 +299,7 @@ namespace OpenIddict.EntityFramework
where application.PostLogoutRedirectUris.Contains(address) where application.PostLogoutRedirectUris.Contains(address)
select application).ToListAsync(cancellationToken); select application).ToListAsync(cancellationToken);
var builder = ImmutableArray.CreateBuilder<TApplication>(); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Count);
foreach (var application in applications) foreach (var application in applications)
{ {
@ -329,7 +330,8 @@ namespace OpenIddict.EntityFramework
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri. /// returns the client applications corresponding to the specified redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -345,7 +347,7 @@ namespace OpenIddict.EntityFramework
where application.RedirectUris.Contains(address) where application.RedirectUris.Contains(address)
select application).ToListAsync(cancellationToken); select application).ToListAsync(cancellationToken);
var builder = ImmutableArray.CreateBuilder<TApplication>(); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Count);
foreach (var application in applications) foreach (var application in applications)
{ {
@ -602,7 +604,18 @@ namespace OpenIddict.EntityFramework
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>

30
src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs

@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -556,7 +556,18 @@ namespace OpenIddict.EntityFramework
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>
@ -580,7 +591,20 @@ namespace OpenIddict.EntityFramework
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>()); return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
} }
return new ValueTask<ImmutableArray<string>>(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<ImmutableArray<string>>(scopes);
} }
/// <summary> /// <summary>

15
src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs

@ -61,7 +61,7 @@ namespace OpenIddict.EntityFramework
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -390,7 +390,18 @@ namespace OpenIddict.EntityFramework
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>

17
src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs

@ -69,7 +69,7 @@ namespace OpenIddict.EntityFramework
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -627,7 +627,18 @@ namespace OpenIddict.EntityFramework
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>
@ -698,7 +709,7 @@ namespace OpenIddict.EntityFramework
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual ValueTask<string> GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {

5
src/OpenIddict.EntityFrameworkCore/OpenIddictEntityFrameworkCoreExtensions.cs

@ -36,6 +36,11 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.AddMemoryCache(); 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<OpenIddictApplication>() builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() .SetDefaultAuthorizationEntity<OpenIddictAuthorization>()
.SetDefaultScopeEntity<OpenIddictScope>() .SetDefaultScopeEntity<OpenIddictScope>()

25
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs

@ -90,7 +90,7 @@ namespace OpenIddict.EntityFrameworkCore
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -325,7 +325,8 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri. /// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -341,7 +342,7 @@ namespace OpenIddict.EntityFrameworkCore
where application.PostLogoutRedirectUris.Contains(address) where application.PostLogoutRedirectUris.Contains(address)
select application).ToListAsync(cancellationToken); select application).ToListAsync(cancellationToken);
var builder = ImmutableArray.CreateBuilder<TApplication>(); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Count);
foreach (var application in applications) foreach (var application in applications)
{ {
@ -372,7 +373,8 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri. /// returns the client applications corresponding to the specified redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -388,7 +390,7 @@ namespace OpenIddict.EntityFrameworkCore
where application.RedirectUris.Contains(address) where application.RedirectUris.Contains(address)
select application).ToListAsync(cancellationToken); select application).ToListAsync(cancellationToken);
var builder = ImmutableArray.CreateBuilder<TApplication>(); var builder = ImmutableArray.CreateBuilder<TApplication>(applications.Count);
foreach (var application in applications) foreach (var application in applications)
{ {
@ -645,7 +647,18 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>

30
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictAuthorizationStore.cs

@ -90,7 +90,7 @@ namespace OpenIddict.EntityFrameworkCore
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -606,7 +606,18 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>
@ -630,7 +641,20 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>()); return new ValueTask<ImmutableArray<string>>(ImmutableArray.Create<string>());
} }
return new ValueTask<ImmutableArray<string>>(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<ImmutableArray<string>>(scopes);
} }
/// <summary> /// <summary>

15
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictScopeStore.cs

@ -78,7 +78,7 @@ namespace OpenIddict.EntityFrameworkCore
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -407,7 +407,18 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>

17
src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictTokenStore.cs

@ -90,7 +90,7 @@ namespace OpenIddict.EntityFrameworkCore
} }
/// <summary> /// <summary>
/// Gets the memory cached associated with the current store. /// Gets the memory cache associated with the current store.
/// </summary> /// </summary>
protected IMemoryCache Cache { get; } protected IMemoryCache Cache { get; }
@ -658,7 +658,18 @@ namespace OpenIddict.EntityFrameworkCore
return new ValueTask<JObject>(new JObject()); return new ValueTask<JObject>(new JObject());
} }
return new ValueTask<JObject>(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>((JObject) properties.DeepClone());
} }
/// <summary> /// <summary>
@ -729,7 +740,7 @@ namespace OpenIddict.EntityFrameworkCore
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual ValueTask<string> GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {

4
src/OpenIddict.MongoDb/OpenIddictMongoDbExtensions.cs

@ -31,7 +31,9 @@ namespace Microsoft.Extensions.DependencyInjection
throw new ArgumentNullException(nameof(builder)); 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<OpenIddictApplication>() builder.SetDefaultApplicationEntity<OpenIddictApplication>()
.SetDefaultAuthorizationEntity<OpenIddictAuthorization>() .SetDefaultAuthorizationEntity<OpenIddictAuthorization>()

4
src/OpenIddict.MongoDb/Resolvers/OpenIddictApplicationStoreResolver.cs

@ -22,9 +22,7 @@ namespace OpenIddict.MongoDb
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictApplicationStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns an application store compatible with the specified application type or throws an /// Returns an application store compatible with the specified application type or throws an

4
src/OpenIddict.MongoDb/Resolvers/OpenIddictAuthorizationStoreResolver.cs

@ -22,9 +22,7 @@ namespace OpenIddict.MongoDb
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider) public OpenIddictAuthorizationStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns an authorization store compatible with the specified authorization type or throws an /// Returns an authorization store compatible with the specified authorization type or throws an

4
src/OpenIddict.MongoDb/Resolvers/OpenIddictScopeStoreResolver.cs

@ -22,9 +22,7 @@ namespace OpenIddict.MongoDb
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider) public OpenIddictScopeStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns a scope store compatible with the specified scope type or throws an /// Returns a scope store compatible with the specified scope type or throws an

4
src/OpenIddict.MongoDb/Resolvers/OpenIddictTokenStoreResolver.cs

@ -22,9 +22,7 @@ namespace OpenIddict.MongoDb
private readonly IServiceProvider _provider; private readonly IServiceProvider _provider;
public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider) public OpenIddictTokenStoreResolver([NotNull] IServiceProvider provider)
{ => _provider = provider;
_provider = provider;
}
/// <summary> /// <summary>
/// Returns a token store compatible with the specified token type or throws an /// Returns a token store compatible with the specified token type or throws an

29
src/OpenIddict.MongoDb/Stores/OpenIddictApplicationStore.cs

@ -12,7 +12,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb
where TApplication : OpenIddictApplication where TApplication : OpenIddictApplication
{ {
public OpenIddictApplicationStore( public OpenIddictApplicationStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context, [NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptions<OpenIddictMongoDbOptions> options) [NotNull] IOptions<OpenIddictMongoDbOptions> options)
{ {
Cache = cache;
Context = context; Context = context;
Options = options; Options = options;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of applications that match the specified query. /// whose result returns the number of applications that match the specified query.
/// </returns> /// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual async Task<long> CountAsync<TResult>(
[NotNull] Func<IQueryable<TApplication>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -193,7 +186,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken); var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TApplication>(Options.Value.ApplicationsCollectionName); var collection = database.GetCollection<TApplication>(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);
} }
/// <summary> /// <summary>
@ -205,7 +199,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified post_logout_redirect_uri. /// returns the client applications corresponding to the specified post_logout_redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByPostLogoutRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -228,7 +223,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result /// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the client applications corresponding to the specified redirect_uri. /// returns the client applications corresponding to the specified redirect_uri.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync([NotNull] string address, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TApplication>> FindByRedirectUriAsync(
[NotNull] string address, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(address)) if (string.IsNullOrEmpty(address))
{ {
@ -394,7 +390,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the permissions associated with the application. /// whose result returns all the permissions associated with the application.
/// </returns> /// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual ValueTask<ImmutableArray<string>> GetPermissionsAsync(
[NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -418,7 +415,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the post_logout_redirect_uri associated with the application. /// whose result returns all the post_logout_redirect_uri associated with the application.
/// </returns> /// </returns>
public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual ValueTask<ImmutableArray<string>> GetPostLogoutRedirectUrisAsync(
[NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {
@ -466,7 +464,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns all the redirect_uri associated with the application. /// whose result returns all the redirect_uri associated with the application.
/// </returns> /// </returns>
public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync([NotNull] TApplication application, CancellationToken cancellationToken) public virtual ValueTask<ImmutableArray<string>> GetRedirectUrisAsync(
[NotNull] TApplication application, CancellationToken cancellationToken)
{ {
if (application == null) if (application == null)
{ {

20
src/OpenIddict.MongoDb/Stores/OpenIddictAuthorizationStore.cs

@ -12,7 +12,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb
where TAuthorization : OpenIddictAuthorization where TAuthorization : OpenIddictAuthorization
{ {
public OpenIddictAuthorizationStore( public OpenIddictAuthorizationStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context, [NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptions<OpenIddictMongoDbOptions> options) [NotNull] IOptions<OpenIddictMongoDbOptions> options)
{ {
Cache = cache;
Context = context; Context = context;
Options = options; Options = options;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of authorizations that match the specified query. /// whose result returns the number of authorizations that match the specified query.
/// </returns> /// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual async Task<long> CountAsync<TResult>(
[NotNull] Func<IQueryable<TAuthorization>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -357,7 +350,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken); var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.Value.AuthorizationsCollectionName); var collection = database.GetCollection<TAuthorization>(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);
} }
/// <summary> /// <summary>
@ -380,7 +374,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken); var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TAuthorization>(Options.Value.AuthorizationsCollectionName); var collection = database.GetCollection<TAuthorization>(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));
} }
/// <summary> /// <summary>
@ -481,7 +476,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the scopes associated with the specified authorization. /// whose result returns the scopes associated with the specified authorization.
/// </returns> /// </returns>
public virtual ValueTask<ImmutableArray<string>> GetScopesAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken) public virtual ValueTask<ImmutableArray<string>> GetScopesAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken)
{ {
if (authorization == null) if (authorization == null)
{ {

14
src/OpenIddict.MongoDb/Stores/OpenIddictScopeStore.cs

@ -12,7 +12,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb
where TScope : OpenIddictScope where TScope : OpenIddictScope
{ {
public OpenIddictScopeStore( public OpenIddictScopeStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context, [NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptions<OpenIddictMongoDbOptions> options) [NotNull] IOptions<OpenIddictMongoDbOptions> options)
{ {
Cache = cache;
Context = context; Context = context;
Options = options; Options = options;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of scopes that match the specified query. /// whose result returns the number of scopes that match the specified query.
/// </returns> /// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual async Task<long> CountAsync<TResult>(
[NotNull] Func<IQueryable<TScope>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -231,7 +224,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken); var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TScope>(Options.Value.ScopesCollectionName); var collection = database.GetCollection<TScope>(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));
} }
/// <summary> /// <summary>

22
src/OpenIddict.MongoDb/Stores/OpenIddictTokenStore.cs

@ -12,7 +12,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -31,20 +30,13 @@ namespace OpenIddict.MongoDb
where TToken : OpenIddictToken where TToken : OpenIddictToken
{ {
public OpenIddictTokenStore( public OpenIddictTokenStore(
[NotNull] IMemoryCache cache,
[NotNull] IOpenIddictMongoDbContext context, [NotNull] IOpenIddictMongoDbContext context,
[NotNull] IOptions<OpenIddictMongoDbOptions> options) [NotNull] IOptions<OpenIddictMongoDbOptions> options)
{ {
Cache = cache;
Context = context; Context = context;
Options = options; Options = options;
} }
/// <summary>
/// Gets the memory cached associated with the current store.
/// </summary>
protected IMemoryCache Cache { get; }
/// <summary> /// <summary>
/// Gets the database context associated with the current store. /// Gets the database context associated with the current store.
/// </summary> /// </summary>
@ -81,7 +73,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the number of tokens that match the specified query. /// whose result returns the number of tokens that match the specified query.
/// </returns> /// </returns>
public virtual async Task<long> CountAsync<TResult>([NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken) public virtual async Task<long> CountAsync<TResult>(
[NotNull] Func<IQueryable<TToken>, IQueryable<TResult>> query, CancellationToken cancellationToken)
{ {
if (query == null) if (query == null)
{ {
@ -270,7 +263,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified application. /// whose result returns the tokens corresponding to the specified application.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TToken>> FindByApplicationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TToken>> FindByApplicationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -280,7 +274,8 @@ namespace OpenIddict.MongoDb
var database = await Context.GetDatabaseAsync(cancellationToken); var database = await Context.GetDatabaseAsync(cancellationToken);
var collection = database.GetCollection<TToken>(Options.Value.TokensCollectionName); var collection = database.GetCollection<TToken>(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));
} }
/// <summary> /// <summary>
@ -292,7 +287,8 @@ namespace OpenIddict.MongoDb
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, /// A <see cref="Task"/> that can be used to monitor the asynchronous operation,
/// whose result returns the tokens corresponding to the specified authorization. /// whose result returns the tokens corresponding to the specified authorization.
/// </returns> /// </returns>
public virtual async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync([NotNull] string identifier, CancellationToken cancellationToken) public virtual async Task<ImmutableArray<TToken>> FindByAuthorizationIdAsync(
[NotNull] string identifier, CancellationToken cancellationToken)
{ {
if (string.IsNullOrEmpty(identifier)) if (string.IsNullOrEmpty(identifier))
{ {
@ -605,7 +601,7 @@ namespace OpenIddict.MongoDb
/// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation, /// A <see cref="ValueTask{TResult}"/> that can be used to monitor the asynchronous operation,
/// whose result returns the token type associated with the specified token. /// whose result returns the token type associated with the specified token.
/// </returns> /// </returns>
public virtual ValueTask<string> GetTokenTypeAsync([NotNull] TToken token, CancellationToken cancellationToken) public virtual ValueTask<string> GetTypeAsync([NotNull] TToken token, CancellationToken cancellationToken)
{ {
if (token == null) if (token == null)
{ {

4
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs

@ -308,10 +308,6 @@ namespace OpenIddict.Server.Internal
return; 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 // 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. // 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 // Note: when using the authorization code grant, ValidateTokenRequest is responsible of rejecting

16
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs

@ -175,10 +175,6 @@ namespace OpenIddict.Server.Internal
return; 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. // Reject the request if the application is not allowed to use the token endpoint.
if (!options.IgnoreEndpointPermissions && if (!options.IgnoreEndpointPermissions &&
!await applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Token)) !await applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Token))
@ -360,16 +356,16 @@ namespace OpenIddict.Server.Internal
var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId); var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId);
Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); 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 // 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. // 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. // 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. // Try to revoke the authorization and the associated tokens.
// If the operation fails, the helpers will automatically log // If the operation fails, the helpers will automatically log

86
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 the client application is known, bind it to the authorization.
if (!string.IsNullOrEmpty(request.ClientId)) if (!string.IsNullOrEmpty(request.ClientId))
{ {
var application = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}"); var application = await applicationManager.FindByClientIdAsync(request.ClientId);
Debug.Assert(application != null, "The client application shouldn't be null."); if (application == null)
{
throw new InvalidOperationException("The application entry cannot be found in the database.");
}
descriptor.ApplicationId = await applicationManager.GetIdAsync(application); descriptor.ApplicationId = await applicationManager.GetIdAsync(application);
} }
@ -142,8 +145,6 @@ namespace OpenIddict.Server.Internal
descriptor.Properties.Add(property); descriptor.Properties.Add(property);
} }
string result = null;
// When reference tokens are enabled or when the token is an authorization code or a // 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. // refresh token, remove the unnecessary properties from the authentication ticket.
if (options.UseReferenceTokens || if (options.UseReferenceTokens ||
@ -168,10 +169,10 @@ namespace OpenIddict.Server.Internal
// substituted to the ciphertext returned by the data format. // substituted to the ciphertext returned by the data format.
var bytes = new byte[256 / 8]; var bytes = new byte[256 / 8];
options.RandomNumberGenerator.GetBytes(bytes); options.RandomNumberGenerator.GetBytes(bytes);
result = Base64UrlEncoder.Encode(bytes);
// Obfuscate the reference identifier so it can be safely stored in the databse. // Note: the default token manager automatically obfuscates the
descriptor.ReferenceId = await tokenManager.ObfuscateReferenceIdAsync(result); // 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. // 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 the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(request.ClientId)) if (!string.IsNullOrEmpty(request.ClientId))
{ {
var application = request.GetProperty($"{OpenIddictConstants.Properties.Application}:{request.ClientId}"); var application = await applicationManager.FindByClientIdAsync(request.ClientId);
Debug.Assert(application != null, "The client application shouldn't be null."); if (application == null)
{
throw new InvalidOperationException("The application entry cannot be found in the database.");
}
descriptor.ApplicationId = await applicationManager.GetIdAsync(application); descriptor.ApplicationId = await applicationManager.GetIdAsync(application);
} }
@ -215,14 +219,16 @@ namespace OpenIddict.Server.Internal
ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, identifier) ticket.SetProperty(OpenIddictConstants.Properties.InternalTokenId, identifier)
.SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, descriptor.AuthorizationId); .SetProperty(OpenIddictConstants.Properties.InternalAuthorizationId, descriptor.AuthorizationId);
if (!string.IsNullOrEmpty(result)) if (options.UseReferenceTokens)
{ {
logger.LogTrace("A new reference token was successfully generated and persisted " + logger.LogTrace("A new reference token was successfully generated and persisted " +
"in the database: {Token} ; {Claims} ; {Properties}.", "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<AuthenticationTicket> ReceiveTokenAsync( private async Task<AuthenticationTicket> ReceiveTokenAsync(
@ -248,30 +254,31 @@ namespace OpenIddict.Server.Internal
if (options.UseReferenceTokens) if (options.UseReferenceTokens)
{ {
// For introspection or revocation requests, this method may be called more than once. token = await tokenManager.FindByReferenceIdAsync(value);
// For reference tokens, this may result in multiple database calls being made. if (token == null)
// 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 = 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 logger.LogWarning("The token type associated with the received token cannot be retrieved. " +
// cannot be found, assume the token is not valid. "This may indicate that the token entry is corrupted.");
token = await tokenManager.FindByReferenceIdAsync(value);
// Store the token as a request property so it can be retrieved if this method is called another time. return null;
request.AddProperty($"{OpenIddictConstants.Properties.ReferenceToken}:{value}", token);
} }
if (token == null) if (!string.Equals(usage, type, StringComparison.OrdinalIgnoreCase))
{ {
logger.LogInformation("The reference token corresponding to the '{Identifier}' " + logger.LogWarning("The token type '{ActualType}' associated with the database entry doesn't match " +
"reference identifier cannot be found in the database.", value); "the expected type: {ExpectedType}.", await tokenManager.GetTypeAsync(token), type);
return null; return null;
} }
@ -305,8 +312,6 @@ namespace OpenIddict.Server.Internal
return null; return null;
} }
request.SetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}", token);
} }
else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode || else if (type == OpenIdConnectConstants.TokenUsages.AuthorizationCode ||
@ -329,26 +334,7 @@ namespace OpenIddict.Server.Internal
return null; return null;
} }
// For introspection or revocation requests, this method may be called more than once. token = await tokenManager.FindByIdAsync(identifier);
// 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);
}
if (token == null) if (token == null)
{ {
logger.LogInformation("The token '{Identifier}' cannot be found in the database.", identifier); logger.LogInformation("The token '{Identifier}' cannot be found in the database.", identifier);

15
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs

@ -7,7 +7,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Primitives;
using AspNet.Security.OpenIdConnect.Server; using AspNet.Security.OpenIdConnect.Server;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -61,10 +60,6 @@ namespace OpenIddict.Server.Internal
return; 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. // Reject the request if the application is not allowed to use the introspection endpoint.
if (!options.IgnoreEndpointPermissions && if (!options.IgnoreEndpointPermissions &&
!await applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Introspection)) !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. // which an entry exists in the database - ensure it is still valid.
if (options.UseReferenceTokens) if (options.UseReferenceTokens)
{ {
// Retrieve the token from the request properties. If it's marked as invalid, return active = false. var token = await tokenManager.FindByIdAsync(identifier);
var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); if (token == null || !await tokenManager.IsValidAsync(token))
Debug.Assert(token != null, "The token shouldn't be null.");
if (!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; context.Active = false;

11
src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs

@ -102,10 +102,6 @@ namespace OpenIddict.Server.Internal
return; 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. // Reject the request if the application is not allowed to use the revocation endpoint.
if (!options.IgnoreEndpointPermissions && if (!options.IgnoreEndpointPermissions &&
!await applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Revocation)) !await applicationManager.HasPermissionAsync(application, OpenIddictConstants.Permissions.Endpoints.Revocation))
@ -215,11 +211,8 @@ namespace OpenIddict.Server.Internal
var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId); var identifier = context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId);
Debug.Assert(!string.IsNullOrEmpty(identifier), "The authentication ticket should contain a token identifier."); 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 = await tokenManager.FindByIdAsync(identifier);
var token = context.Request.GetProperty($"{OpenIddictConstants.Properties.Token}:{identifier}"); if (token == null || await tokenManager.IsRevokedAsync(token))
Debug.Assert(token != null, "The token shouldn't be null.");
if (await tokenManager.IsRevokedAsync(token))
{ {
logger.LogInformation("The token '{Identifier}' was not revoked because " + logger.LogInformation("The token '{Identifier}' was not revoked because " +
"it was already marked as invalid.", identifier); "it was already marked as invalid.", identifier);

16
src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs

@ -51,6 +51,8 @@ namespace OpenIddict.Server.Internal
{ {
var options = (OpenIddictServerOptions) context.Options; var options = (OpenIddictServerOptions) context.Options;
var tokenManager = GetTokenManager(context.HttpContext.RequestServices);
Debug.Assert(context.Request.IsAuthorizationRequest() || Debug.Assert(context.Request.IsAuthorizationRequest() ||
context.Request.IsTokenRequest(), context.Request.IsTokenRequest(),
"The request should be an authorization or token request."); "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 token revocation was explicitly disabled, none of the following security routines apply.
if (!options.DisableTokenStorage) if (!options.DisableTokenStorage)
{ {
var token = context.Request.GetProperty(OpenIddictConstants.Properties.Token + ":" + var token = await tokenManager.FindByIdAsync(context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId));
context.Ticket.GetProperty(OpenIddictConstants.Properties.InternalTokenId)); if (token == null)
Debug.Assert(token != null, "The token shouldn't be 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, // 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. // mark the authorization code or the refresh token as redeemed to prevent future reuses.

58
src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs

@ -5,6 +5,7 @@
*/ */
using System; using System;
using System.Diagnostics;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using AspNet.Security.OAuth.Validation; using AspNet.Security.OAuth.Validation;
@ -60,21 +61,30 @@ namespace OpenIddict.Validation.Internal
return; return;
} }
// Extract the encrypted payload from the token. If it's null or empty, // Optimization: avoid extracting/decrypting the token payload if the token is not an access token.
// assume the token is not a reference token and consider it as invalid. var type = await manager.GetTypeAsync(token);
var payload = await manager.GetPayloadAsync(token); if (string.IsNullOrEmpty(type))
if (string.IsNullOrEmpty(payload))
{ {
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(); context.HandleResponse();
return; return;
} }
// Ensure the access token is still valid (i.e was not marked as revoked). if (!string.Equals(type, OpenIddictConstants.TokenTypes.AccessToken, StringComparison.OrdinalIgnoreCase))
if (!await manager.IsValidAsync(token))
{ {
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(); context.HandleResponse();
return; return;
@ -113,7 +123,37 @@ namespace OpenIddict.Validation.Internal
public override async Task ValidateToken([NotNull] ValidateTokenContext context) public override async Task ValidateToken([NotNull] ValidateTokenContext context)
{ {
var logger = GetLogger(context.HttpContext.RequestServices);
var options = (OpenIddictValidationOptions) context.Options; 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<IOpenIddictTokenManager>();
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) if (options.EnableAuthorizationValidation)
{ {
// Note: the authorization manager is deliberately not injected using constructor injection // Note: the authorization manager is deliberately not injected using constructor injection
@ -128,8 +168,6 @@ namespace OpenIddict.Validation.Internal
.ToString()); .ToString());
} }
var logger = GetLogger(context.HttpContext.RequestServices);
var identifier = context.Ticket.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId); var identifier = context.Ticket.Properties.GetProperty(OpenIddictConstants.Properties.InternalAuthorizationId);
if (!string.IsNullOrEmpty(identifier)) if (!string.IsNullOrEmpty(identifier))
{ {

92
test/OpenIddict.Core.Tests/OpenIddictCoreBuilderTests.cs

@ -391,6 +391,40 @@ namespace OpenIddict.Core.Tests
Assert.IsType(type, store); 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<IOptions<OpenIddictCoreOptions>>().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<IOptions<OpenIddictCoreOptions>>().Value;
Assert.True(options.DisableEntityCaching);
}
[Fact] [Fact]
public void SetDefaultApplicationEntity_ThrowsAnExceptionForNullType() public void SetDefaultApplicationEntity_ThrowsAnExceptionForNullType()
{ {
@ -591,6 +625,40 @@ namespace OpenIddict.Core.Tests
Assert.Equal(typeof(CustomToken), options.DefaultTokenType); 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<ArgumentException>(() => 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<IOptions<OpenIddictCoreOptions>>().Value;
Assert.Equal(42, options.EntityCacheLimit);
}
private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services) private static OpenIddictCoreBuilder CreateBuilder(IServiceCollection services)
=> services.AddOpenIddict().AddCore(); => services.AddOpenIddict().AddCore();
@ -610,10 +678,11 @@ namespace OpenIddict.Core.Tests
private class ClosedGenericApplicationManager : OpenIddictApplicationManager<CustomApplication> private class ClosedGenericApplicationManager : OpenIddictApplicationManager<CustomApplication>
{ {
public ClosedGenericApplicationManager( public ClosedGenericApplicationManager(
IOpenIddictApplicationCache<CustomApplication> cache,
IOpenIddictApplicationStoreResolver resolver, IOpenIddictApplicationStoreResolver resolver,
ILogger<OpenIddictApplicationManager<CustomApplication>> logger, ILogger<OpenIddictApplicationManager<CustomApplication>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -622,10 +691,11 @@ namespace OpenIddict.Core.Tests
where TApplication : class where TApplication : class
{ {
public OpenGenericApplicationManager( public OpenGenericApplicationManager(
IOpenIddictApplicationCache<TApplication> cache,
IOpenIddictApplicationStoreResolver resolver, IOpenIddictApplicationStoreResolver resolver,
ILogger<OpenIddictApplicationManager<TApplication>> logger, ILogger<OpenIddictApplicationManager<TApplication>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -633,10 +703,11 @@ namespace OpenIddict.Core.Tests
private class ClosedGenericAuthorizationManager : OpenIddictAuthorizationManager<CustomAuthorization> private class ClosedGenericAuthorizationManager : OpenIddictAuthorizationManager<CustomAuthorization>
{ {
public ClosedGenericAuthorizationManager( public ClosedGenericAuthorizationManager(
IOpenIddictAuthorizationCache<CustomAuthorization> cache,
IOpenIddictAuthorizationStoreResolver resolver, IOpenIddictAuthorizationStoreResolver resolver,
ILogger<OpenIddictAuthorizationManager<CustomAuthorization>> logger, ILogger<OpenIddictAuthorizationManager<CustomAuthorization>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -645,10 +716,11 @@ namespace OpenIddict.Core.Tests
where TAuthorization : class where TAuthorization : class
{ {
public OpenGenericAuthorizationManager( public OpenGenericAuthorizationManager(
IOpenIddictAuthorizationCache<TAuthorization> cache,
IOpenIddictAuthorizationStoreResolver resolver, IOpenIddictAuthorizationStoreResolver resolver,
ILogger<OpenIddictAuthorizationManager<TAuthorization>> logger, ILogger<OpenIddictAuthorizationManager<TAuthorization>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -656,10 +728,11 @@ namespace OpenIddict.Core.Tests
private class ClosedGenericScopeManager : OpenIddictScopeManager<CustomScope> private class ClosedGenericScopeManager : OpenIddictScopeManager<CustomScope>
{ {
public ClosedGenericScopeManager( public ClosedGenericScopeManager(
IOpenIddictScopeCache<CustomScope> cache,
IOpenIddictScopeStoreResolver resolver, IOpenIddictScopeStoreResolver resolver,
ILogger<OpenIddictScopeManager<CustomScope>> logger, ILogger<OpenIddictScopeManager<CustomScope>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -668,10 +741,11 @@ namespace OpenIddict.Core.Tests
where TScope : class where TScope : class
{ {
public OpenGenericScopeManager( public OpenGenericScopeManager(
IOpenIddictScopeCache<TScope> cache,
IOpenIddictScopeStoreResolver resolver, IOpenIddictScopeStoreResolver resolver,
ILogger<OpenIddictScopeManager<TScope>> logger, ILogger<OpenIddictScopeManager<TScope>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -679,10 +753,11 @@ namespace OpenIddict.Core.Tests
private class ClosedGenericTokenManager : OpenIddictTokenManager<CustomToken> private class ClosedGenericTokenManager : OpenIddictTokenManager<CustomToken>
{ {
public ClosedGenericTokenManager( public ClosedGenericTokenManager(
IOpenIddictTokenCache<CustomToken> cache,
IOpenIddictTokenStoreResolver resolver, IOpenIddictTokenStoreResolver resolver,
ILogger<OpenIddictTokenManager<CustomToken>> logger, ILogger<OpenIddictTokenManager<CustomToken>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }
@ -691,10 +766,11 @@ namespace OpenIddict.Core.Tests
where TToken : class where TToken : class
{ {
public OpenGenericTokenManager( public OpenGenericTokenManager(
IOpenIddictTokenCache<TToken> cache,
IOpenIddictTokenStoreResolver resolver, IOpenIddictTokenStoreResolver resolver,
ILogger<OpenIddictTokenManager<TToken>> logger, ILogger<OpenIddictTokenManager<TToken>> logger,
IOptions<OpenIddictCoreOptions> options) IOptions<OpenIddictCoreOptions> options)
: base(resolver, logger, options) : base(cache, resolver, logger, options)
{ {
} }
} }

14
test/OpenIddict.MongoDb.Tests/OpenIddictMongoDbExtensionsTests.cs

@ -42,20 +42,6 @@ namespace OpenIddict.MongoDb.Tests
Assert.Equal("configuration", exception.ParamName); 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] [Fact]
public void UseMongoDb_RegistersDefaultEntities() public void UseMongoDb_RegistersDefaultEntities()
{ {

1
test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictApplicationStoreResolverTests.cs

@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests
private static OpenIddictApplicationStore<MyApplication> CreateStore() private static OpenIddictApplicationStore<MyApplication> CreateStore()
=> new Mock<OpenIddictApplicationStore<MyApplication>>( => new Mock<OpenIddictApplicationStore<MyApplication>>(
Mock.Of<IMemoryCache>(),
Mock.Of<IOpenIddictMongoDbContext>(), Mock.Of<IOpenIddictMongoDbContext>(),
Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object; Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object;

1
test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictAuthorizationStoreResolverTests.cs

@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests
private static OpenIddictAuthorizationStore<MyAuthorization> CreateStore() private static OpenIddictAuthorizationStore<MyAuthorization> CreateStore()
=> new Mock<OpenIddictAuthorizationStore<MyAuthorization>>( => new Mock<OpenIddictAuthorizationStore<MyAuthorization>>(
Mock.Of<IMemoryCache>(),
Mock.Of<IOpenIddictMongoDbContext>(), Mock.Of<IOpenIddictMongoDbContext>(),
Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object; Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object;

1
test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictScopeStoreResolverTests.cs

@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests
private static OpenIddictScopeStore<MyScope> CreateStore() private static OpenIddictScopeStore<MyScope> CreateStore()
=> new Mock<OpenIddictScopeStore<MyScope>>( => new Mock<OpenIddictScopeStore<MyScope>>(
Mock.Of<IMemoryCache>(),
Mock.Of<IOpenIddictMongoDbContext>(), Mock.Of<IOpenIddictMongoDbContext>(),
Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object; Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object;

1
test/OpenIddict.MongoDb.Tests/Resolvers/OpenIddictTokenStoreResolverTests.cs

@ -69,7 +69,6 @@ namespace OpenIddict.MongoDb.Tests
private static OpenIddictTokenStore<MyToken> CreateStore() private static OpenIddictTokenStore<MyToken> CreateStore()
=> new Mock<OpenIddictTokenStore<MyToken>>( => new Mock<OpenIddictTokenStore<MyToken>>(
Mock.Of<IMemoryCache>(),
Mock.Of<IOpenIddictMongoDbContext>(), Mock.Of<IOpenIddictMongoDbContext>(),
Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object; Mock.Of<IOptions<OpenIddictMongoDbOptions>>()).Object;

12
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(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -1058,7 +1058,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -1316,7 +1316,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code has already been redeemed.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
@ -1409,7 +1409,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token has already been redeemed.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
@ -1484,7 +1484,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsRedeemedAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -1554,7 +1554,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }

14
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Introspection.cs

@ -413,7 +413,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.False((bool) response[OpenIddictConstants.Claims.Active]); Assert.False((bool) response[OpenIddictConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
} }
[Fact] [Fact]
@ -466,6 +466,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -557,6 +560,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -652,6 +658,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -721,6 +730,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("QaTk2f6UPe9trKismGBJr0OIs0KqpvNrqRsJqGuJAAI", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));

4
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.Revocation.cs

@ -475,7 +475,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.Empty(response.GetParameters()); Assert.Empty(response.GetParameters());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny<OpenIddictToken>(), It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny<OpenIddictToken>(), It.IsAny<CancellationToken>()), Times.Never());
} }
@ -524,7 +524,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.Empty(response.GetParameters()); Assert.Empty(response.GetParameters());
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
} }

366
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<string>(), It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null));
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.RefreshToken));
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Fact] [Fact]
public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenIdentifier() public async Task DeserializeAccessToken_ReturnsNullForMissingReferenceTokenIdentifier()
{ {
@ -155,6 +265,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null)); .Returns(new ValueTask<string>(result: null));
}); });
@ -195,7 +308,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Single(response.GetParameters()); Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIddictConstants.Claims.Active]); Assert.False((bool) response[OpenIddictConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
} }
@ -210,6 +323,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -253,7 +369,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Single(response.GetParameters()); Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIddictConstants.Claims.Active]); Assert.False((bool) response[OpenIddictConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
} }
@ -273,6 +389,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -318,7 +437,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Single(response.GetParameters()); Assert.Single(response.GetParameters());
Assert.False((bool) response[OpenIddictConstants.Claims.Active]); Assert.False((bool) response[OpenIddictConstants.Claims.Active]);
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce()); Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); 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<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -545,6 +667,118 @@ namespace OpenIddict.Server.Internal.Tests
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null));
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AccessToken));
});
var server = CreateAuthorizationServer(builder =>
{
builder.Services.AddSingleton(CreateApplicationManager(instance =>
{
var application = new OpenIddictApplication();
instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny<CancellationToken>()))
.ReturnsAsync(application);
instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.ClientTypes.Confidential));
instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Fact] [Fact]
public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenIdentifier() public async Task DeserializeAuthorizationCode_ReturnsNullForMissingReferenceTokenIdentifier()
{ {
@ -556,6 +790,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AuthorizationCode));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null)); .Returns(new ValueTask<string>(result: null));
}); });
@ -612,6 +849,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AuthorizationCode));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -676,6 +916,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AuthorizationCode));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -753,6 +996,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.AuthorizationCode));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -1003,7 +1249,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.AccessToken); Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); 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<string>(), It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), 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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), Times.AtLeastOnce());
}
[Fact] [Fact]
public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenIdentifier() public async Task DeserializeRefreshToken_ReturnsNullForMissingReferenceTokenIdentifier()
{ {
@ -1121,6 +1445,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.RefreshToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null)); .Returns(new ValueTask<string>(result: null));
}); });
@ -1160,6 +1487,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.RefreshToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -1207,6 +1537,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.RefreshToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -1265,6 +1598,9 @@ namespace OpenIddict.Server.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("HQnldPTjH_9m85GcS-5PPYaCxmJTt1umxOa2y9ggVUQ", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIdConnectConstants.TokenUsages.RefreshToken));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
@ -1446,7 +1782,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.AccessToken); Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
format.Verify(mock => mock.Unprotect("2YotnFZFEjr1zCsicMWpAA"), Times.Once()); 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<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1");
}); });
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
@ -1528,9 +1861,6 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.AccessToken); Assert.NotNull(response.AccessToken);
Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync(
It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.CreateAsync( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) && 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<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1");
}); });
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
@ -1830,9 +2157,6 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.Code); Assert.NotNull(response.Code);
Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync(
It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.CreateAsync( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && 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<CancellationToken>())) instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56")); .Returns(new ValueTask<string>("3E228451-1555-46F7-A471-951EFBA23A56"));
instance.Setup(mock => mock.ObfuscateReferenceIdAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
.ReturnsAsync("B1F0D503-55A4-4B03-B05B-EF07713C18E1");
}); });
var server = CreateAuthorizationServer(builder => var server = CreateAuthorizationServer(builder =>
@ -2171,15 +2492,12 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.RefreshToken); Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.ObfuscateReferenceIdAsync(
It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
Mock.Get(manager).Verify(mock => mock.CreateAsync( Mock.Get(manager).Verify(mock => mock.CreateAsync(
It.Is<OpenIddictTokenDescriptor>(descriptor => It.Is<OpenIddictTokenDescriptor>(descriptor =>
descriptor.ExpirationDate == new DateTimeOffset(2017, 01, 02, 00, 00, 00, TimeSpan.Zero) && 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.CreationDate == new DateTimeOffset(2017, 01, 01, 00, 00, 00, TimeSpan.Zero) &&
descriptor.Payload != null && descriptor.Payload != null &&
descriptor.ReferenceId == "B1F0D503-55A4-4B03-B05B-EF07713C18E1" && descriptor.ReferenceId != null &&
descriptor.Subject == "Bob le Magnifique" && descriptor.Subject == "Bob le Magnifique" &&
descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken), descriptor.Type == OpenIdConnectConstants.TokenTypeHints.RefreshToken),
It.IsAny<CancellationToken>()), Times.Once()); It.IsAny<CancellationToken>()), Times.Once());

18
test/OpenIddict.Server.Tests/Internal/OpenIddictServerProviderTests.cs

@ -474,7 +474,7 @@ namespace OpenIddict.Server.Internal.Tests
}); });
// Assert // Assert
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -549,7 +549,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified authorization code is no longer valid.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -615,7 +615,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.RefreshToken); Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -685,7 +685,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error); Assert.Equal(OpenIddictConstants.Errors.InvalidGrant, response.Error);
Assert.Equal("The specified refresh token is no longer valid.", response.ErrorDescription); 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
@ -743,7 +743,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.Null(response.RefreshToken); Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RedeemAsync(token, It.IsAny<CancellationToken>()), Times.Never());
} }
@ -835,7 +835,7 @@ namespace OpenIddict.Server.Internal.Tests
// Assert // Assert
Assert.NotNull(response.RefreshToken); Assert.NotNull(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Once());
@ -923,7 +923,7 @@ namespace OpenIddict.Server.Internal.Tests
Assert.NotNull(response.AccessToken); Assert.NotNull(response.AccessToken);
Assert.Null(response.RefreshToken); Assert.Null(response.RefreshToken);
Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("60FFF7EA-F98E-437B-937E-5073CC313103", It.IsAny<CancellationToken>()), Times.AtLeastOnce());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[0], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[1], It.IsAny<CancellationToken>()), Times.Never());
Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(tokens[2], It.IsAny<CancellationToken>()), Times.Never());
@ -1576,6 +1576,7 @@ namespace OpenIddict.Server.Internal.Tests
Action<Mock<OpenIddictApplicationManager<OpenIddictApplication>>> configuration = null) Action<Mock<OpenIddictApplicationManager<OpenIddictApplication>>> configuration = null)
{ {
var manager = new Mock<OpenIddictApplicationManager<OpenIddictApplication>>( var manager = new Mock<OpenIddictApplicationManager<OpenIddictApplication>>(
Mock.Of<IOpenIddictApplicationCache<OpenIddictApplication>>(),
Mock.Of<IOpenIddictApplicationStoreResolver>(), Mock.Of<IOpenIddictApplicationStoreResolver>(),
Mock.Of<ILogger<OpenIddictApplicationManager<OpenIddictApplication>>>(), Mock.Of<ILogger<OpenIddictApplicationManager<OpenIddictApplication>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());
@ -1589,6 +1590,7 @@ namespace OpenIddict.Server.Internal.Tests
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null) Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null)
{ {
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>( var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationCache<OpenIddictAuthorization>>(),
Mock.Of<IOpenIddictAuthorizationStoreResolver>(), Mock.Of<IOpenIddictAuthorizationStoreResolver>(),
Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(), Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());
@ -1602,6 +1604,7 @@ namespace OpenIddict.Server.Internal.Tests
Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> configuration = null) Action<Mock<OpenIddictScopeManager<OpenIddictScope>>> configuration = null)
{ {
var manager = new Mock<OpenIddictScopeManager<OpenIddictScope>>( var manager = new Mock<OpenIddictScopeManager<OpenIddictScope>>(
Mock.Of<IOpenIddictScopeCache<OpenIddictScope>>(),
Mock.Of<IOpenIddictScopeStoreResolver>(), Mock.Of<IOpenIddictScopeStoreResolver>(),
Mock.Of<ILogger<OpenIddictScopeManager<OpenIddictScope>>>(), Mock.Of<ILogger<OpenIddictScopeManager<OpenIddictScope>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());
@ -1615,6 +1618,7 @@ namespace OpenIddict.Server.Internal.Tests
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null) Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{ {
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>( var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenCache<OpenIddictToken>>(),
Mock.Of<IOpenIddictTokenStoreResolver>(), Mock.Of<IOpenIddictTokenStoreResolver>(),
Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(), Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());

150
test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs

@ -101,7 +101,7 @@ namespace OpenIddict.Validation.Internal.Tests
} }
[Fact] [Fact]
public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken() public async Task DecryptToken_ReturnsFailedResultForMissingTokenType()
{ {
// Arrange // Arrange
var token = new OpenIddictToken(); var token = new OpenIddictToken();
@ -111,7 +111,7 @@ namespace OpenIddict.Validation.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(result: null)); .Returns(new ValueTask<string>(result: null));
}); });
@ -132,29 +132,60 @@ namespace OpenIddict.Validation.Internal.Tests
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), Times.Once());
} }
[Fact] [Fact]
public async Task DecryptToken_ReturnsFailedResultForReferenceTokenWithInvalidStatus() public async Task DecryptToken_ReturnsFailedResultForIncompatibleTokenType()
{ {
// Arrange // Arrange
var token = new OpenIddictToken(); var token = new OpenIddictToken();
var format = new Mock<ISecureDataFormat<AuthenticationTicket>>(); var manager = CreateTokenManager(instance =>
format.Setup(mock => mock.Unprotect("valid-reference-token-payload")) {
.Returns(value: null); instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()), Times.Once());
}
[Fact]
public async Task DecryptToken_ReturnsFailedResultForNonReferenceToken()
{
// Arrange
var token = new OpenIddictToken();
var manager = CreateTokenManager(instance => var manager = CreateTokenManager(instance =>
{ {
instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("valid-reference-token-payload")); .Returns(new ValueTask<string>(OpenIddictConstants.TokenTypes.AccessToken));
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(false); .Returns(new ValueTask<string>(result: null));
}); });
var server = CreateResourceServer(builder => 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Never());
} }
[Fact] [Fact]
@ -194,11 +223,11 @@ namespace OpenIddict.Validation.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.TokenTypes.AccessToken));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("invalid-reference-token-payload")); .Returns(new ValueTask<string>("invalid-reference-token-payload"));
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}); });
var server = CreateResourceServer(builder => 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<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), Times.Once());
format.Verify(mock => mock.Unprotect("invalid-reference-token-payload"), Times.Once()); format.Verify(mock => mock.Unprotect("invalid-reference-token-payload"), Times.Once());
} }
[Fact] [Fact]
public async Task DecryptToken_ReturnsValidResultForValidReferenceToken() public async Task ValidateToken_ReturnsFailedResultForInvalidReferenceToken()
{ {
// Arrange // Arrange
var token = new OpenIddictToken(); var token = new OpenIddictToken();
@ -248,17 +276,98 @@ namespace OpenIddict.Validation.Internal.Tests
instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>())) instance.Setup(mock => mock.FindByReferenceIdAsync("valid-reference-token-id", It.IsAny<CancellationToken>()))
.ReturnsAsync(token); .ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.TokenTypes.AccessToken));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("valid-reference-token-payload")); .Returns(new ValueTask<string>("valid-reference-token-payload"));
instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero)));
instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero)));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("4392E01A-1BC4-4776-8450-EC267C2B708A"));
instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.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<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()), 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<ISecureDataFormat<AuthenticationTicket>>();
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<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.GetTypeAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>(OpenIddictConstants.TokenTypes.AccessToken));
instance.Setup(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("valid-reference-token-payload"));
instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero))); .Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2018, 01, 01, 00, 00, 00, TimeSpan.Zero)));
instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>())) instance.Setup(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero))); .Returns(new ValueTask<DateTimeOffset?>(new DateTimeOffset(2918, 01, 01, 00, 00, 00, TimeSpan.Zero)));
instance.Setup(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()))
.Returns(new ValueTask<string>("4392E01A-1BC4-4776-8450-EC267C2B708A"));
instance.Setup(mock => mock.FindByIdAsync("4392E01A-1BC4-4776-8450-EC267C2B708A", It.IsAny<CancellationToken>()))
.ReturnsAsync(token);
instance.Setup(mock => mock.IsValidAsync(token, It.IsAny<CancellationToken>()))
.ReturnsAsync(true);
}); });
var server = CreateResourceServer(builder => var server = CreateResourceServer(builder =>
@ -297,6 +406,7 @@ namespace OpenIddict.Validation.Internal.Tests
Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetPayloadAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetCreationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetExpirationDateAsync(token, It.IsAny<CancellationToken>()), Times.Once());
Mock.Get(manager).Verify(mock => mock.GetIdAsync(token, It.IsAny<CancellationToken>()), Times.Once());
format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once()); format.Verify(mock => mock.Unprotect("valid-reference-token-payload"), Times.Once());
} }
@ -620,6 +730,7 @@ namespace OpenIddict.Validation.Internal.Tests
Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null) Action<Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>> configuration = null)
{ {
var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>( var manager = new Mock<OpenIddictAuthorizationManager<OpenIddictAuthorization>>(
Mock.Of<IOpenIddictAuthorizationCache<OpenIddictAuthorization>>(),
Mock.Of<IOpenIddictAuthorizationStoreResolver>(), Mock.Of<IOpenIddictAuthorizationStoreResolver>(),
Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(), Mock.Of<ILogger<OpenIddictAuthorizationManager<OpenIddictAuthorization>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());
@ -633,6 +744,7 @@ namespace OpenIddict.Validation.Internal.Tests
Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null) Action<Mock<OpenIddictTokenManager<OpenIddictToken>>> configuration = null)
{ {
var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>( var manager = new Mock<OpenIddictTokenManager<OpenIddictToken>>(
Mock.Of<IOpenIddictTokenCache<OpenIddictToken>>(),
Mock.Of<IOpenIddictTokenStoreResolver>(), Mock.Of<IOpenIddictTokenStoreResolver>(),
Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(), Mock.Of<ILogger<OpenIddictTokenManager<OpenIddictToken>>>(),
Mock.Of<IOptions<OpenIddictCoreOptions>>()); Mock.Of<IOptions<OpenIddictCoreOptions>>());

Loading…
Cancel
Save