Browse Source

Make transparent request caching an opt-in feature

pull/258/head
Kévin Chalet 9 years ago
parent
commit
135da2f611
  1. 7
      samples/Mvc.Server/Startup.cs
  2. 18
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs
  3. 18
      src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs
  4. 12
      src/OpenIddict.Core/OpenIddictBuilder.cs
  5. 14
      src/OpenIddict.Core/OpenIddictExtensions.cs
  6. 9
      src/OpenIddict.Core/OpenIddictOptions.cs
  7. 2
      src/OpenIddict.Core/project.json
  8. 68
      test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Authentication.cs
  9. 32
      test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Serialization.cs
  10. 49
      test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.Session.cs
  11. 4
      test/OpenIddict.Core.Tests/Infrastructure/OpenIddictProviderTests.cs
  12. 18
      test/OpenIddict.Core.Tests/OpenIddictBuilderTests.cs
  13. 18
      test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs

7
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.

18
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<OpenIddictServices<TApplication, TAuthorization, TScope, TToken>>();
// 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;

18
src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs

@ -25,8 +25,20 @@ namespace OpenIddict.Infrastructure {
var services = context.HttpContext.RequestServices.GetRequiredService<OpenIddictServices<TApplication, TAuthorization, TScope, TToken>>();
// 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<OpenIddictServices<TApplication, TAuthorization, TScope, TToken>>();
// 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;

12
src/OpenIddict.Core/OpenIddictBuilder.cs

@ -597,6 +597,18 @@ namespace Microsoft.AspNetCore.Builder {
return Configure(options => options.LogoutEndpointPath = path);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The <see cref="OpenIddictBuilder"/>.</returns>
public virtual OpenIddictBuilder EnableRequestCaching() {
return Configure(options => options.EnableRequestCaching = true);
}
/// <summary>
/// Enables the revocation endpoint.
/// </summary>

14
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<IOptions<OpenIddictOptions>>().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<IDistributedCache>();
options.Cache = app.ApplicationServices.GetService<IDistributedCache>();
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 " +

9
src/OpenIddict.Core/OpenIddictOptions.cs

@ -26,6 +26,15 @@ namespace OpenIddict {
/// </summary>
public IDistributedCache Cache { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool EnableRequestCaching { get; set; }
/// <summary>
/// Gets the OAuth2/OpenID Connect flows enabled for this application.
/// </summary>

2
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": {

68
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<IDistributedCache>();
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<object>();
@ -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());

32
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<IDistributedCache>();
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

49
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<DistributedCacheEntryOptions>()), Times.Once());
}
[Fact]
public async Task HandleLogoutRequest_RequestsAreNotHandledLocally() {
// Arrange
var server = CreateAuthorizationServer(builder => {
builder.Services.AddSingleton(CreateApplicationManager(instance => {
var application = Mock.Of<object>();
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

4
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";

18
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<IOptions<OpenIddictOptions>>();
// Assert
Assert.True(options.Value.EnableRequestCaching);
}
[Fact]
public void EnableRevocationEndpoint_RevocationEndpointIsEnabled() {
// Arrange

18
test/OpenIddict.Core.Tests/OpenIddictExtensionsTests.cs

@ -30,7 +30,6 @@ namespace OpenIddict.Core.Tests {
[Theory]
[InlineData(typeof(IDataProtectionProvider))]
[InlineData(typeof(IDistributedCache))]
[InlineData(typeof(OpenIddictApplicationManager<object>))]
[InlineData(typeof(OpenIddictAuthorizationManager<object>))]
[InlineData(typeof(OpenIddictScopeManager<object>))]
@ -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<object, object, object, object>()
.EnableRequestCaching();
var builder = new ApplicationBuilder(services.BuildServiceProvider());
// Act and assert
var exception = Assert.Throws<InvalidOperationException>(() => 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

Loading…
Cancel
Save