/* * 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); } } }