From d79fa71b4141b8d282cd1a9395b44af5b428a477 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 21 Aug 2026 15:39:57 +0800 Subject: [PATCH] Derive the OpenIddict opt-in URLs from the configured server endpoints --- .../UnitOfWorkMiddleware_Relational_Tests.cs | 14 +++++--- .../AbpOpenIddictAspNetCoreModule.cs | 36 ++++++++++++++++--- ...enIddictTokenEndpoint_Integration_Tests.cs | 13 ++++++- .../OpenIddictTokenIntegrationTestModule.cs | 7 ++++ 4 files changed, 59 insertions(+), 11 deletions(-) diff --git a/framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs index 72924981b5..2d20c60005 100644 --- a/framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Uow.Tests/Volo/Abp/AspNetCore/Uow/UnitOfWorkMiddleware_Relational_Tests.cs @@ -124,18 +124,22 @@ public class UnitOfWorkMiddleware_Relational_Tests : AbpWebApplicationFactoryInt EnableCompleteOnResponseStarting(); var name = Guid.NewGuid().ToString("N"); + HttpResponseMessage response = null; + Exception surfaced = null; try { - var response = await Client.GetAsync("/api/uow-visibility/insert-then-throw-in-serialization?name=" + name); + response = await Client.GetAsync("/api/uow-visibility/insert-then-throw-in-serialization?name=" + name); await response.Content.ReadAsStringAsync(); } - catch (Exception) + catch (Exception ex) { + surfaced = ex; } - // The action saved the row, then serializing the result failed before the response started. The error - // response is written by the upstream exception middleware after the request unit of work is disposed, - // so response-start completion must not commit the failed request. + // The action ran and saved the row, then serializing the result failed. The request must therefore + // fail with a server error (not a 404 or a success), and the error response, written by the upstream + // exception middleware after the request unit of work is disposed, must not commit the failed request. + (surfaced != null || (response != null && (int)response.StatusCode >= 500)).ShouldBeTrue(); (await CountAsync(name)).ShouldBe(0); } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs index ea03453c52..7e025a80b8 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; +using System.Linq; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using OpenIddict.Abstractions; using OpenIddict.Server; using Volo.Abp.AspNetCore.MultiTenancy; @@ -48,12 +50,36 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule options.RemoveClientIdClaim(); }); - // Complete tokens/authorizations/sessions written while processing OpenIddict requests before the response starts. - Configure(options => + // Complete data written while processing OpenIddict requests before the response starts. + // Derived from the configured server endpoint paths so remapped endpoints are covered too. + context.Services.AddOptions() + .Configure>((uowOptions, serverOptions) => + { + foreach (var path in GetServerEndpointPaths(serverOptions.Value)) + { + uowOptions.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains(path); + } + }); + } + + private static IEnumerable GetServerEndpointPaths(OpenIddictServerOptions serverOptions) + { + var endpoints = serverOptions.TokenEndpointUris + .Concat(serverOptions.AuthorizationEndpointUris) + .Concat(serverOptions.DeviceAuthorizationEndpointUris) + .Concat(serverOptions.PushedAuthorizationEndpointUris) + .Concat(serverOptions.EndSessionEndpointUris) + .Concat(serverOptions.RevocationEndpointUris) + .Concat(serverOptions.EndUserVerificationEndpointUris); + + foreach (var uri in endpoints) { - options.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains("/connect"); - options.CompleteUnitOfWorkOnResponseStartingUrls.AddIfNotContains("/device"); - }); + var path = uri.IsAbsoluteUri ? uri.AbsolutePath : "/" + uri.OriginalString.TrimStart('/'); + if (path.Length > 1) + { + yield return path; + } + } } private void AddOpenIddictServer(IServiceCollection services) diff --git a/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs b/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs index cf11cffdf3..3f8fd1a855 100644 --- a/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs +++ b/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenEndpoint_Integration_Tests.cs @@ -34,7 +34,7 @@ public class OpenIddictTokenEndpoint_Integration_Tests : AbpWebApplicationFactor [Fact] public async Task Token_Row_Is_Committed_Before_The_Connect_Token_Response_Is_Sent() { - // The OpenIddict module opts "/connect" in by default. + // The OpenIddict module opts its configured endpoint paths (including "/connect/token") in by default. var response = await RequestTokenAsync(); response.StatusCode.ShouldBe(HttpStatusCode.OK); @@ -55,4 +55,15 @@ public class OpenIddictTokenEndpoint_Integration_Tests : AbpWebApplicationFactor response.StatusCode.ShouldBe(HttpStatusCode.OK); TokenCountAtResponseStart.ShouldBe(0); } + + [Fact] + public void The_Configured_OpenIddict_Endpoint_Paths_Are_Opted_In() + { + // The opt-in list is derived from the configured server endpoints, so a non-"/connect" endpoint + // like "/device" is covered, and the custom "/my-custom/token" endpoint the test host registered + // is followed too - a hardcoded "/connect" prefix would miss both. + Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/connect/token"); + Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/device"); + Options.CompleteUnitOfWorkOnResponseStartingUrls.ShouldContain("/my-custom/token"); + } } diff --git a/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs b/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs index 1319045ca6..29e9c5a180 100644 --- a/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs +++ b/modules/openiddict/test/Volo.Abp.OpenIddict.AspNetCore.Tests/Volo/Abp/OpenIddict/Integration/OpenIddictTokenIntegrationTestModule.cs @@ -60,6 +60,13 @@ public class OpenIddictTokenIntegrationTestModule : AbpModule { context.Services.AddSingleton(); + // A remapped token endpoint, so the tests can prove the opt-in list is derived from the configured + // server endpoints (custom endpoints are followed) rather than a hardcoded "/connect" prefix. + Configure(options => + { + options.TokenEndpointUris.Add(new Uri("my-custom/token", UriKind.Relative)); + }); + // The OpenIddict controllers (including the token endpoint) live in a referenced assembly. context.Services.GetSingletonInstance() .ApplicationParts.AddIfNotContains(typeof(AbpOpenIddictAspNetCoreModule).Assembly);