diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 00f290d0..a0eb8754 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -53,6 +53,13 @@ namespace Mvc.Server { // During development, you can disable the HTTPS requirement. .DisableHttpsRequirement() + // When request caching is enabled, authorization and logout requests + // are stored in the distributed cache by OpenIddict and the user agent + // is redirected to the same page with a single parameter (request_id). + // This allows flowing large OpenID Connect requests even when using + // an external authentication provider like Google, Facebook or Twitter. + .EnableRequestCaching() + // Register a new ephemeral key, that is discarded when the application // shuts down. Tokens signed using this key are automatically invalidated. // This method should only be used during development. diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index 7fcdf177..b5597670 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -52,8 +52,20 @@ namespace OpenIddict.Infrastructure { } // If a request_id parameter can be found in the authorization request, - // restore the complete authorization request stored in the distributed cache. + // restore the complete authorization request from the distributed cache. if (!string.IsNullOrEmpty(context.Request.RequestId)) { + // Return an error if request caching support was not enabled. + if (!services.Options.EnableRequestCaching) { + services.Logger.LogError("The authorization request was rejected because " + + "request caching support was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The request_id parameter is not supported."); + + return; + } + // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; @@ -313,7 +325,7 @@ namespace OpenIddict.Infrastructure { // If no request_id parameter can be found in the current request, assume the OpenID Connect request // was not serialized yet and store the entire payload in the distributed cache to make it easier // to flow across requests and internal/external authentication/registration workflows. - if (string.IsNullOrEmpty(context.Request.RequestId)) { + if (services.Options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) { // Generate a request identifier. Note: using a crypto-secure // random number generator is not necessary in this case. context.Request.RequestId = Guid.NewGuid().ToString(); @@ -358,7 +370,7 @@ namespace OpenIddict.Infrastructure { var services = context.HttpContext.RequestServices.GetRequiredService>(); // Remove the authorization request from the distributed cache. - if (!string.IsNullOrEmpty(context.Request.RequestId)) { + if (services.Options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) { // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs index 200ac26c..f332e83f 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs @@ -25,8 +25,20 @@ namespace OpenIddict.Infrastructure { var services = context.HttpContext.RequestServices.GetRequiredService>(); // If a request_id parameter can be found in the logout request, - // restore the complete logout request stored in the distributed cache. + // restore the complete logout request from the distributed cache. if (!string.IsNullOrEmpty(context.Request.RequestId)) { + // Return an error if request caching support was not enabled. + if (!services.Options.EnableRequestCaching) { + services.Logger.LogError("The logout request was rejected because " + + "request caching support was not enabled."); + + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The request_id parameter is not supported."); + + return; + } + // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; @@ -91,7 +103,7 @@ namespace OpenIddict.Infrastructure { // If no request_id parameter can be found in the current request, assume the OpenID Connect // request was not serialized yet and store the entire payload in the distributed cache // to make it easier to flow across requests and internal/external logout workflows. - if (string.IsNullOrEmpty(context.Request.RequestId)) { + if (services.Options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) { // Generate a request identifier. Note: using a crypto-secure // random number generator is not necessary in this case. context.Request.RequestId = Guid.NewGuid().ToString(); @@ -134,7 +146,7 @@ namespace OpenIddict.Infrastructure { var services = context.HttpContext.RequestServices.GetRequiredService>(); // Remove the logout request from the distributed cache. - if (!string.IsNullOrEmpty(context.Request.RequestId)) { + if (services.Options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) { // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index df168812..f168fd4a 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -597,6 +597,18 @@ namespace Microsoft.AspNetCore.Builder { return Configure(options => options.LogoutEndpointPath = path); } + /// + /// Enables request caching, so that both authorization and logout requests + /// are automatically stored in the distributed cache, which allows flowing + /// large payloads across requests. Enabling this option is recommended + /// when using external authentication providers or when large GET or POST + /// OpenID Connect authorization requests support is required. + /// + /// The . + public virtual OpenIddictBuilder EnableRequestCaching() { + return Configure(options => options.EnableRequestCaching = true); + } + /// /// Enables the revocation endpoint. /// diff --git a/src/OpenIddict.Core/OpenIddictExtensions.cs b/src/OpenIddict.Core/OpenIddictExtensions.cs index 184b7d30..05b96d69 100644 --- a/src/OpenIddict.Core/OpenIddictExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictExtensions.cs @@ -43,9 +43,9 @@ namespace Microsoft.AspNetCore.Builder { TokenType = typeof(TToken) }; - // Register the services required by the OpenID Connect server middleware. + // Register the authentication services, as they are + // required by the OpenID Connect server middleware. builder.Services.AddAuthentication(); - builder.Services.AddDistributedMemoryCache(); builder.Configure(options => { // Register the OpenID Connect server provider in the OpenIddict options. @@ -75,10 +75,18 @@ namespace Microsoft.AspNetCore.Builder { // Resolve the OpenIddict options from the DI container. var options = app.ApplicationServices.GetRequiredService>().Value; + // When no distributed cache has been registered in the options, + // try to resolve it from the dependency injection container. if (options.Cache == null) { - options.Cache = app.ApplicationServices.GetRequiredService(); + options.Cache = app.ApplicationServices.GetService(); + + if (options.EnableRequestCaching && options.Cache == null) { + throw new InvalidOperationException("A distributed cache implementation must be registered in the OpenIddict options " + + "or in the dependency injection container when enabling request caching support."); + } } + // Ensure at least one signing certificate/key has been registered. if (options.SigningCredentials.Count == 0) { throw new InvalidOperationException("At least one signing key must be registered. Consider registering a X.509 " + "certificate using 'services.AddOpenIddict().AddSigningCertificate()' or call " + diff --git a/src/OpenIddict.Core/OpenIddictOptions.cs b/src/OpenIddict.Core/OpenIddictOptions.cs index a89f054c..8d570bea 100644 --- a/src/OpenIddict.Core/OpenIddictOptions.cs +++ b/src/OpenIddict.Core/OpenIddictOptions.cs @@ -26,6 +26,15 @@ namespace OpenIddict { /// public IDistributedCache Cache { get; set; } + /// + /// Gets or sets a boolean indicating whether request caching should be enabled. + /// When enabled, both authorization and logout requests are automatically stored + /// in the distributed cache, which allows flowing large payloads across requests. + /// Enabling this option is recommended when using external authentication providers + /// or when large GET or POST OpenID Connect authorization requests support is required. + /// + public bool EnableRequestCaching { get; set; } + /// /// Gets the OAuth2/OpenID Connect flows enabled for this application. /// diff --git a/src/OpenIddict.Core/project.json b/src/OpenIddict.Core/project.json index a513ecb0..c00f7b71 100644 --- a/src/OpenIddict.Core/project.json +++ b/src/OpenIddict.Core/project.json @@ -37,7 +37,7 @@ "CryptoHelper": "2.0.0", "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, "Microsoft.AspNetCore.Diagnostics.Abstractions": "1.0.0", - "Microsoft.Extensions.Caching.Memory": "1.0.0" + "Microsoft.Extensions.Caching.Abstractions": "1.0.0" }, "frameworks": { diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs index 6cb262f5..817ed416 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs @@ -22,9 +22,9 @@ namespace OpenIddict.Core.Tests.Infrastructure { var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { ClientId = "Fabrikam", RedirectUri = "http://www.fabrikam.com/path", - Request = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwOi8vd3d3LmZhYnJpa2FtLmNvbSIsImF1ZCI6Imh" + - "0dHA6Ly93d3cuY29udG9zby5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZC" + - "I6IkZhYnJpa2FtIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL3d3dy5mYWJyaWthbS5jb20vcGF0aCJ9.", + Request = "eyJhbGciOiJub25lIn0.eyJpc3MiOiJodHRwOi8vd3d3LmZhYnJpa2FtLmNvbSIsImF1ZCI6Imh0" + + "dHA6Ly93d3cuY29udG9zby5jb20iLCJyZXNwb25zZV90eXBlIjoiY29kZSIsImNsaWVudF9pZCI6" + + "IkZhYnJpa2FtIiwicmVkaXJlY3RfdXJpIjoiaHR0cDovL3d3dy5mYWJyaWthbS5jb20vcGF0aCJ9.", ResponseType = OpenIdConnectConstants.ResponseTypes.Code, Scope = OpenIdConnectConstants.Scopes.OpenId }); @@ -56,7 +56,7 @@ namespace OpenIddict.Core.Tests.Infrastructure { } [Fact] - public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected() { + public async Task ExtractAuthorizationRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() { // Arrange var server = CreateAuthorizationServer(); @@ -64,10 +64,28 @@ namespace OpenIddict.Core.Tests.Infrastructure { // Act var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7", - ResponseType = OpenIdConnectConstants.ResponseTypes.Code + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The request_id parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractAuthorizationRequest_InvalidRequestIdParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddDistributedMemoryCache(); + + builder.EnableRequestCaching(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" }); // Assert @@ -382,6 +400,8 @@ namespace OpenIddict.Core.Tests.Infrastructure { })); builder.Services.AddSingleton(cache.Object); + + builder.EnableRequestCaching(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -415,28 +435,6 @@ namespace OpenIddict.Core.Tests.Infrastructure { [InlineData("token")] public async Task HandleAuthorizationRequest_RequestsAreNotHandledLocally(string type) { // Arrange - var request = new OpenIdConnectRequest { - ClientId = "Fabrikam", - Nonce = "n-0S6_WzA2Mj", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = type, - Scope = OpenIdConnectConstants.Scopes.OpenId - }; - - var stream = new MemoryStream(); - using (var writer = new BsonWriter(stream)) { - writer.CloseOutput = false; - - var serializer = JsonSerializer.CreateDefault(); - serializer.Serialize(writer, request); - } - - var cache = new Mock(); - - cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + - "b2ee7815-5579-4ff7-86b0-ba671b939d96")) - .ReturnsAsync(stream.ToArray()); - var server = CreateAuthorizationServer(builder => { builder.Services.AddSingleton(CreateApplicationManager(instance => { var application = Mock.Of(); @@ -455,15 +453,17 @@ namespace OpenIddict.Core.Tests.Infrastructure { instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); })); - - builder.Services.AddSingleton(cache.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { - RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" + ClientId = "Fabrikam", + Nonce = "n-0S6_WzA2Mj", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = type, + Scope = OpenIdConnectConstants.Scopes.OpenId }); // Assert @@ -510,6 +510,8 @@ namespace OpenIddict.Core.Tests.Infrastructure { })); builder.Services.AddSingleton(cache.Object); + + builder.EnableRequestCaching(); }); var client = new OpenIdConnectClient(server.CreateClient()); diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs index bbe0c702..56215744 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs @@ -1,11 +1,7 @@ -using System.IO; -using System.Threading.Tasks; +using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; -using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; using Moq; -using Newtonsoft.Json; -using Newtonsoft.Json.Bson; using Xunit; namespace OpenIddict.Core.Tests.Infrastructure { @@ -13,26 +9,6 @@ namespace OpenIddict.Core.Tests.Infrastructure { [Fact] public async Task SerializeAuthorizationCode_AuthorizationCodeIsAutomaticallyPersisted() { // Arrange - var request = new OpenIdConnectRequest { - ClientId = "Fabrikam", - RedirectUri = "http://www.fabrikam.com/path", - ResponseType = OpenIdConnectConstants.ResponseTypes.Code - }; - - var stream = new MemoryStream(); - using (var writer = new BsonWriter(stream)) { - writer.CloseOutput = false; - - var serializer = JsonSerializer.CreateDefault(); - serializer.Serialize(writer, request); - } - - var cache = new Mock(); - - cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + - "b2ee7815-5579-4ff7-86b0-ba671b939d96")) - .ReturnsAsync(stream.ToArray()); - var manager = CreateTokenManager(instance => { instance.Setup(mock => mock.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode)) .ReturnsAsync("3E228451-1555-46F7-A471-951EFBA23A56"); @@ -53,15 +29,15 @@ namespace OpenIddict.Core.Tests.Infrastructure { })); builder.Services.AddSingleton(manager); - - builder.Services.AddSingleton(cache.Object); }); var client = new OpenIdConnectClient(server.CreateClient()); // Act var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { - RequestId = "b2ee7815-5579-4ff7-86b0-ba671b939d96" + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code }); // Assert diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs index 62085483..9b6d11d4 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs @@ -9,7 +9,7 @@ using Xunit; namespace OpenIddict.Core.Tests.Infrastructure { public partial class OpenIddictProviderTests { [Fact] - public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() { + public async Task ExtractLogoutRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() { // Arrange var server = CreateAuthorizationServer(); @@ -20,6 +20,27 @@ namespace OpenIddict.Core.Tests.Infrastructure { RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" }); + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); + Assert.Equal("The request_id parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddDistributedMemoryCache(); + + builder.EnableRequestCaching(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + // Assert Assert.Equal(OpenIdConnectConstants.Errors.InvalidRequest, response.Error); Assert.Equal("Invalid request: timeout expired.", response.ErrorDescription); @@ -65,6 +86,8 @@ namespace OpenIddict.Core.Tests.Infrastructure { })); builder.Services.AddSingleton(cache.Object); + + builder.EnableRequestCaching(); }); var client = new OpenIdConnectClient(server.CreateClient()); @@ -86,6 +109,30 @@ namespace OpenIddict.Core.Tests.Infrastructure { It.IsAny()), Times.Once()); } + [Fact] + public async Task HandleLogoutRequest_RequestsAreNotHandledLocally() { + // Arrange + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(CreateApplicationManager(instance => { + var application = Mock.Of(); + + instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path")) + .ReturnsAsync(application); + })); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(LogoutEndpoint, new OpenIdConnectRequest { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.Equal("af0ifjsldkj", response.State); + } + [Fact] public async Task ApplyLogoutResponse_ErroredRequestIsNotHandledLocallyWhenStatusCodeMiddlewareIsEnabled() { // Arrange diff --git a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs index 7567df89..ab48bfab 100644 --- a/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs @@ -110,6 +110,10 @@ namespace OpenIddict.Core.Tests.Infrastructure { return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); } + else if (context.Request.Path == LogoutEndpoint) { + return context.Authentication.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme); + } + else if (context.Request.Path == UserinfoEndpoint) { context.Response.Headers[HeaderNames.ContentType] = "application/json"; diff --git a/test/OpenIddict.Core.Tests/OpenIddictBuilderTests.cs b/test/OpenIddict.Core.Tests/OpenIddictBuilderTests.cs index 49e309eb..bed6a409 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictBuilderTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictBuilderTests.cs @@ -626,6 +626,24 @@ namespace OpenIddict.Core.Tests { Assert.Equal("/endpoint-path", options.Value.LogoutEndpointPath); } + [Fact] + public void EnableRequestCaching_RequestCachingIsEnabled() { + // Arrange + var services = new ServiceCollection(); + services.AddOptions(); + + var builder = new OpenIddictBuilder(services); + + // Act + builder.EnableRequestCaching(); + + var provider = services.BuildServiceProvider(); + var options = provider.GetRequiredService>(); + + // Assert + Assert.True(options.Value.EnableRequestCaching); + } + [Fact] public void EnableRevocationEndpoint_RevocationEndpointIsEnabled() { // Arrange diff --git a/test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs index e6017333..89cce806 100644 --- a/test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs @@ -30,7 +30,6 @@ namespace OpenIddict.Core.Tests { [Theory] [InlineData(typeof(IDataProtectionProvider))] - [InlineData(typeof(IDistributedCache))] [InlineData(typeof(OpenIddictApplicationManager))] [InlineData(typeof(OpenIddictAuthorizationManager))] [InlineData(typeof(OpenIddictScopeManager))] @@ -47,6 +46,23 @@ namespace OpenIddict.Core.Tests { Assert.Contains(services, service => service.ServiceType == type); } + [Fact] + public void UseOpenIddict_AnExceptionIsThrownWhenNoDistributedCacheIsRegisteredIfRequestCachingIsEnabled() { + // Arrange + var services = new ServiceCollection(); + + services.AddOpenIddict() + .EnableRequestCaching(); + + var builder = new ApplicationBuilder(services.BuildServiceProvider()); + + // Act and assert + var exception = Assert.Throws(() => builder.UseOpenIddict()); + + Assert.Equal("A distributed cache implementation must be registered in the OpenIddict options " + + "or in the dependency injection container when enabling request caching support.", exception.Message); + } + [Fact] public void UseOpenIddict_AnExceptionIsThrownWhenNoSigningCredentialsIsRegistered() { // Arrange