diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 9e917f21..bcef21ac 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -154,17 +154,31 @@ namespace OpenIddict.Core } /// - /// Retrieves an application using its post_logout_redirect_uri. + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. /// - /// The post_logout_redirect_uri associated with the application. + /// The post_logout_redirect_uri associated with the applications. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result - /// returns the client application corresponding to the post_logout_redirect_uri. + /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public virtual Task FindByLogoutRedirectUri(string url, CancellationToken cancellationToken) + public virtual Task FindByLogoutRedirectUri(string address, CancellationToken cancellationToken) { - return Store.FindByLogoutRedirectUri(url, cancellationToken); + return Store.FindByLogoutRedirectUri(address, cancellationToken); + } + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified redirect_uri. + /// + public virtual Task FindByRedirectUri(string address, CancellationToken cancellationToken) + { + return Store.FindByRedirectUri(address, cancellationToken); } /// @@ -469,9 +483,8 @@ namespace OpenIddict.Core var address = await Store.GetRedirectUriAsync(application, cancellationToken); if (!string.IsNullOrEmpty(address)) { - Uri uri; // Ensure the redirect_uri is a valid and absolute URL. - if (!Uri.TryCreate(address, UriKind.Absolute, out uri)) + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) { throw new ArgumentException("The redirect_uri must be an absolute URL."); } @@ -487,9 +500,8 @@ namespace OpenIddict.Core address = await Store.GetLogoutRedirectUriAsync(application, cancellationToken); if (!string.IsNullOrEmpty(address)) { - Uri uri; // Ensure the post_logout_redirect_uri is a valid and absolute URL. - if (!Uri.TryCreate(address, UriKind.Absolute, out uri)) + if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) { throw new ArgumentException("The post_logout_redirect_uri must be an absolute URL."); } @@ -502,6 +514,34 @@ namespace OpenIddict.Core } } + /// + /// Validates the specified post_logout_redirect_uri. + /// + /// The address that should be compared to the post_logout_redirect_uri stored in the database. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns a boolean indicating whether the post_logout_redirect_uri was valid. + /// + public virtual async Task ValidateLogoutRedirectUriAsync(string address, CancellationToken cancellationToken) + { + // Warning: SQL engines like Microsoft SQL Server are known to use case-insensitive lookups by default. + // To ensure a case-sensitive comparison is used, string.Equals(Ordinal) is manually called here. + foreach (var application in await Store.FindByLogoutRedirectUri(address, cancellationToken)) + { + // Note: the post_logout_redirect_uri must be compared using case-sensitive "Simple String Comparison". + if (string.Equals(address, await Store.GetLogoutRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) + { + return true; + } + } + + Logger.LogWarning("Client validation failed because '{PostLogoutRedirectUri}' " + + "was not a valid post_logout_redirect_uri.", address); + + return false; + } + /// /// Validates the redirect_uri associated with an application. /// @@ -519,15 +559,17 @@ namespace OpenIddict.Core throw new ArgumentNullException(nameof(application)); } - if (!string.Equals(address, await Store.GetRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) + // Note: the redirect_uri must be compared using case-sensitive "Simple String Comparison". + // See http://openid.net/specs/openid-connect-core-1_0.html#AuthRequest for more information. + if (string.Equals(address, await Store.GetRedirectUriAsync(application, cancellationToken), StringComparison.Ordinal)) { - Logger.LogWarning("Client validation failed because {RedirectUri} was not a valid redirect_uri " + - "for {Client}", address, await GetDisplayNameAsync(application, cancellationToken)); - - return false; + return true; } - return true; + Logger.LogWarning("Client validation failed because '{RedirectUri}' was not a valid redirect_uri " + + "for '{Client}'.", address, await GetDisplayNameAsync(application, cancellationToken)); + + return false; } /// diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs index 1a212ff0..972340bf 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -60,15 +60,26 @@ namespace OpenIddict.Core Task FindByClientIdAsync(string identifier, CancellationToken cancellationToken); /// - /// Retrieves an application using its post_logout_redirect_uri. + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. /// - /// The post_logout_redirect_uri associated with the application. + /// The post_logout_redirect_uri associated with the applications. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result - /// returns the client application corresponding to the post_logout_redirect_uri. + /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - Task FindByLogoutRedirectUri(string url, CancellationToken cancellationToken); + Task FindByLogoutRedirectUri(string address, CancellationToken cancellationToken); + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified redirect_uri. + /// + Task FindByRedirectUri(string address, CancellationToken cancellationToken); /// /// Retrieves the client identifier associated with an application. diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 264489db..e6aaa966 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -156,17 +156,31 @@ namespace OpenIddict.EntityFrameworkCore } /// - /// Retrieves an application using its post_logout_redirect_uri. + /// Retrieves all the applications associated with the specified post_logout_redirect_uri. /// - /// The post_logout_redirect_uri associated with the application. + /// The post_logout_redirect_uri associated with the applications. /// The that can be used to abort the operation. /// /// A that can be used to monitor the asynchronous operation, whose result - /// returns the client application corresponding to the post_logout_redirect_uri. + /// returns the client applications corresponding to the specified post_logout_redirect_uri. /// - public virtual Task FindByLogoutRedirectUri(string url, CancellationToken cancellationToken) + public virtual Task FindByLogoutRedirectUri(string address, CancellationToken cancellationToken) { - return Applications.SingleOrDefaultAsync(application => application.LogoutRedirectUri == url, cancellationToken); + return Applications.Where(application => application.LogoutRedirectUri == address).ToArrayAsync(cancellationToken); + } + + /// + /// Retrieves all the applications associated with the specified redirect_uri. + /// + /// The redirect_uri associated with the applications. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the client applications corresponding to the specified redirect_uri. + /// + public virtual Task FindByRedirectUri(string address, CancellationToken cancellationToken) + { + return Applications.Where(application => application.RedirectUri == address).ToArrayAsync(cancellationToken); } /// diff --git a/src/OpenIddict/OpenIddictProvider.Session.cs b/src/OpenIddict/OpenIddictProvider.Session.cs index 0dfc66c1..6eae960a 100644 --- a/src/OpenIddict/OpenIddictProvider.Session.cs +++ b/src/OpenIddict/OpenIddictProvider.Session.cs @@ -88,21 +88,17 @@ namespace OpenIddict var logger = context.HttpContext.RequestServices.GetRequiredService>>(); // If an optional post_logout_redirect_uri was provided, validate it. - if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri) && + !await applications.ValidateLogoutRedirectUriAsync(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted)) { - var application = await applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted); - if (application == null) - { - logger.LogError("The logout request was rejected because the client application corresponding " + - "to the specified post_logout_redirect_uri was not found in the database: " + - "'{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri); + logger.LogError("The logout request was rejected because the specified post_logout_redirect_uri " + + "was invalid: '{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidClient, - description: "Invalid post_logout_redirect_uri."); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid post_logout_redirect_uri."); - return; - } + return; } context.Validate(); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs index 678a0972..c4c288d6 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs @@ -64,8 +64,8 @@ namespace OpenIddict.Tests // Arrange var manager = CreateApplicationManager(instance => { - instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(value: null); + instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(false); }); var server = CreateAuthorizationServer(builder => @@ -85,7 +85,7 @@ namespace OpenIddict.Tests Assert.Equal(OpenIdConnectConstants.Errors.InvalidClient, response.Error); Assert.Equal("Invalid post_logout_redirect_uri.", response.ErrorDescription); - Mock.Get(manager).Verify(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny()), Times.Once()); } [Fact] @@ -98,10 +98,8 @@ namespace OpenIddict.Tests { builder.Services.AddSingleton(CreateApplicationManager(instance => { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(application); + instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); })); builder.Services.AddSingleton(cache.Object); @@ -137,10 +135,8 @@ namespace OpenIddict.Tests { builder.Services.AddSingleton(CreateApplicationManager(instance => { - var application = new OpenIddictApplication(); - - instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(application); + instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(true); })); }); @@ -165,8 +161,8 @@ namespace OpenIddict.Tests { builder.Services.AddSingleton(CreateApplicationManager(instance => { - instance.Setup(mock => mock.FindByLogoutRedirectUri("http://www.fabrikam.com/path", It.IsAny())) - .ReturnsAsync(value: null); + instance.Setup(mock => mock.ValidateLogoutRedirectUriAsync("http://www.fabrikam.com/path", It.IsAny())) + .ReturnsAsync(false); })); builder.EnableAuthorizationEndpoint("/logout-status-code-middleware");