From 43beb6487708d2904d37123f60a7a73c4dd333b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 6 Jan 2020 15:58:01 +0100 Subject: [PATCH] Port the end session endpoint integration tests --- ...erverAspNetCoreIntegrationTests.Session.cs | 58 ++ ...penIddictServerIntegrationTests.Session.cs | 574 ++++++++++++++++++ ...ddictServerOwinIntegrationTests.Session.cs | 58 ++ 3 files changed, 690 insertions(+) create mode 100644 test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs create mode 100644 test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs create mode 100644 test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs diff --git a/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs new file mode 100644 index 00000000..1fd8b9fc --- /dev/null +++ b/test/OpenIddict.Server.AspNetCore.IntegrationTests/OpenIddictServerAspNetCoreIntegrationTests.Session.cs @@ -0,0 +1,58 @@ +/* + * 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.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.Server.FunctionalTests; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Server.AspNetCore.FunctionalTests +{ + public partial class OpenIddictServerAspNetCoreIntegrationTests : OpenIddictServerIntegrationTests + { + [Fact(Skip = "The handler responsible of rejecting such requests has not been ported yet.")] + public async Task ExtractLogoutRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'request_id' parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() + { + // Arrange + var client = CreateClient(options => + { + options.Services.AddDistributedMemoryCache(); + + options.UseAspNetCore() + .EnableLogoutEndpointCaching(); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified 'request_id' parameter is invalid.", response.ErrorDescription); + } + } +} diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs new file mode 100644 index 00000000..93f79b47 --- /dev/null +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs @@ -0,0 +1,574 @@ +/* + * 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.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using OpenIddict.Abstractions; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.FunctionalTests +{ + public abstract partial class OpenIddictServerIntegrationTests + { + [Theory] + [InlineData(nameof(HttpMethod.Delete))] + [InlineData(nameof(HttpMethod.Head))] + [InlineData(nameof(HttpMethod.Options))] + [InlineData(nameof(HttpMethod.Put))] + [InlineData(nameof(HttpMethod.Trace))] + public async Task ExtractLogoutRequest_UnexpectedMethodReturnsAnError(string method) + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.SendAsync(method, "/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified HTTP method is not valid.", response.ErrorDescription); + } + + [Theory] + [InlineData("custom_error", null, null)] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, null, "custom_uri")] + [InlineData(null, null, null)] + public async Task ExtractLogoutRequest_AllowsRejectingRequest(string error, string description, string uri) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Reject(error, description, uri); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task ExtractLogoutRequest_AllowsHandlingResponse() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", new + { + name = "Bob le Bricoleur" + }); + + context.HandleRequest(); + + return default; + })); + }); + + // Act + var response = await client.GetAsync("/connect/logout"); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task ExtractLogoutRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.GetAsync("/connect/logout"); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Theory] + [InlineData("/path", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] + [InlineData("/tmp/file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] + [InlineData("C:\\tmp\\file.xml", "The 'post_logout_redirect_uri' parameter must be a valid absolute URL.")] + [InlineData("http://www.fabrikam.com/path#param=value", "The 'post_logout_redirect_uri' parameter must not include a fragment.")] + public async Task ValidateLogoutRequest_RequestIsRejectedWhenRedirectUriIsInvalid(string address, string message) + { + // Arrange + var client = CreateClient(); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + PostLogoutRedirectUri = address + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal(message, response.ErrorDescription); + } + + [Fact] + public async Task ValidateLogoutRequest_RequestIsRejectedWhenNoMatchingApplicationIsFound() + { + // Arrange + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .Returns(AsyncEnumerable.Empty()); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified 'post_logout_redirect_uri' parameter is not valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateLogoutRequest_RequestIsRejectedWhenNoMatchingApplicationIsGrantedEndpointPermission() + { + // Arrange + var applications = new[] + { + new OpenIddictApplication(), + new OpenIddictApplication() + }; + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .Returns(applications.ToAsyncEnumerable()); + + mock.Setup(manager => manager.HasPermissionAsync(applications[0], Permissions.Endpoints.Logout, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasPermissionAsync(applications[1], Permissions.Endpoints.Logout, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.Configure(options => options.IgnoreEndpointPermissions = false); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified 'post_logout_redirect_uri' parameter is not valid.", response.ErrorDescription); + + Mock.Get(manager).Verify(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(applications[0], Permissions.Endpoints.Logout, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(applications[1], Permissions.Endpoints.Logout, It.IsAny()), Times.Once()); + } + + [Fact] + public async Task ValidateLogoutRequest_RequestIsValidatedWhenMatchingApplicationIsFound() + { + // Arrange + var applications = new[] + { + new OpenIddictApplication(), + new OpenIddictApplication(), + new OpenIddictApplication() + }; + + var manager = CreateApplicationManager(mock => + { + mock.Setup(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .Returns(applications.ToAsyncEnumerable()); + + mock.Setup(manager => manager.HasPermissionAsync(applications[0], Permissions.Endpoints.Logout, It.IsAny())) + .ReturnsAsync(false); + + mock.Setup(manager => manager.HasPermissionAsync(applications[1], Permissions.Endpoints.Logout, It.IsAny())) + .ReturnsAsync(true); + + mock.Setup(manager => manager.HasPermissionAsync(applications[2], Permissions.Endpoints.Logout, It.IsAny())) + .ReturnsAsync(false); + }); + + var client = CreateClient(options => + { + options.Services.AddSingleton(manager); + + options.SetLogoutEndpointUris("/signout"); + options.Configure(options => options.IgnoreEndpointPermissions = false); + }); + + // Act + var response = await client.PostAsync("/signout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.Equal("af0ifjsldkj", response.State); + + Mock.Get(manager).Verify(manager => manager.FindByPostLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(applications[0], Permissions.Endpoints.Logout, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(applications[1], Permissions.Endpoints.Logout, It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(manager => manager.HasPermissionAsync(applications[2], Permissions.Endpoints.Logout, It.IsAny()), Times.Never()); + } + + [Theory] + [InlineData("custom_error", null, null)] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, null, "custom_uri")] + [InlineData(null, null, null)] + public async Task ValidateLogoutRequest_AllowsRejectingRequest(string error, string description, string uri) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Reject(error, description, uri); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task ValidateLogoutRequest_AllowsHandlingResponse() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", new + { + name = "Bob le Bricoleur" + }); + + context.HandleRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task ValidateLogoutRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Theory] + [InlineData("custom_error", null, null)] + [InlineData("custom_error", "custom_description", null)] + [InlineData("custom_error", "custom_description", "custom_uri")] + [InlineData(null, "custom_description", null)] + [InlineData(null, "custom_description", "custom_uri")] + [InlineData(null, null, "custom_uri")] + [InlineData(null, null, null)] + public async Task HandleLogoutRequest_AllowsRejectingRequest(string error, string description, string uri) + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Reject(error, description, uri); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal(error ?? Errors.InvalidRequest, response.Error); + Assert.Equal(description, response.ErrorDescription); + Assert.Equal(uri, response.ErrorUri); + } + + [Fact] + public async Task HandleLogoutRequest_AllowsHandlingResponse() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", new + { + name = "Bob le Bricoleur" + }); + + context.HandleRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task HandleLogoutRequest_AllowsSkippingHandler() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.SkipRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Magnifique", (string) response["name"]); + } + + [Fact] + public async Task ApplyLogoutResponse_AllowsHandlingResponse() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Transaction.SetProperty("custom_response", new + { + name = "Bob le Bricoleur" + }); + + context.HandleRequest(); + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest()); + + // Assert + Assert.Equal("Bob le Bricoleur", (string) response["name"]); + } + + [Fact] + public async Task ApplyLogoutResponse_ResponseContainsCustomParameters() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Response["custom_parameter"] = "custom_value"; + context.Response["parameter_with_multiple_values"] = new[] + { + "custom_value_1", + "custom_value_2" + }; + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path" + }); + + // Assert + Assert.Equal("custom_value", (string) response["custom_parameter"]); + Assert.Equal(new[] { "custom_value_1", "custom_value_2" }, (string[]) response["parameter_with_multiple_values"]); + } + + [Fact] + public async Task ApplyLogoutResponse_DoesNotSetStateWhenUserIsNotRedirected() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/signout"); + }); + + // Act + var response = await client.PostAsync("/signout", new OpenIddictRequest + { + State = "af0ifjsldkj" + }); + + // Assert + Assert.Null(response.State); + } + + [Fact] + public async Task ApplyLogoutResponse_FlowsStateWhenRedirectUriIsUsed() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/signout"); + }); + + // Act + var response = await client.PostAsync("/signout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.Equal("af0ifjsldkj", response.State); + } + + [Fact] + public async Task ApplyLogoutResponse_DoesNotOverrideStateSetByApplicationCode() + { + // Arrange + var client = CreateClient(options => + { + options.EnableDegradedMode(); + options.SetLogoutEndpointUris("/signout"); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + context.Response.State = "custom_state"; + + return default; + })); + }); + + // Act + var response = await client.PostAsync("/signout", new OpenIddictRequest + { + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.Equal("custom_state", response.State); + } + } +} diff --git a/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs new file mode 100644 index 00000000..b0e3fd61 --- /dev/null +++ b/test/OpenIddict.Server.Owin.IntegrationTests/OpenIddictServerOwinIntegrationTests.Session.cs @@ -0,0 +1,58 @@ +/* + * 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.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using OpenIddict.Server.FunctionalTests; +using Xunit; +using static OpenIddict.Abstractions.OpenIddictConstants; + +namespace OpenIddict.Server.Owin.FunctionalTests +{ + public partial class OpenIddictServerOwinIntegrationTests : OpenIddictServerIntegrationTests + { + [Fact(Skip = "The handler responsible of rejecting such requests has not been ported yet.")] + public async Task ExtractLogoutRequest_RequestIdParameterIsRejectedWhenRequestCachingIsDisabled() + { + // Arrange + var client = CreateClient(options => options.EnableDegradedMode()); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The 'request_id' parameter is not supported.", response.ErrorDescription); + } + + [Fact] + public async Task ExtractLogoutRequest_InvalidRequestIdParameterIsRejected() + { + // Arrange + var client = CreateClient(options => + { + options.Services.AddDistributedMemoryCache(); + + options.UseOwin() + .EnableLogoutEndpointCaching(); + }); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + RequestId = "EFAF3596-F868-497F-96BB-AA2AD1F8B7E7" + }); + + // Assert + Assert.Equal(Errors.InvalidRequest, response.Error); + Assert.Equal("The specified 'request_id' parameter is invalid.", response.ErrorDescription); + } + } +}