From 53a9a0c94be767ce625acf945cdb3b1042a82e2b Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 17 Jul 2026 10:06:13 +0800 Subject: [PATCH] Ignore `OpenIddictAuthorization` navigation changes when creating tokens --- .../AbpOpenIddictEntityFrameworkCoreModule.cs | 1 + .../AbpOpenIddictEntityChangeOptions_Tests.cs | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityChangeOptions_Tests.cs diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs index a12f4161c5..eee54a0031 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.EntityFrameworkCore/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityFrameworkCoreModule.cs @@ -30,6 +30,7 @@ public class AbpOpenIddictEntityFrameworkCoreModule : AbpModule Configure(options => { options.IgnoredNavigationEntitySelectors.Add("DisableOpenIddictApplication", type => type == typeof(OpenIddictApplication)); + options.IgnoredNavigationEntitySelectors.Add("DisableOpenIddictAuthorization", type => type == typeof(OpenIddictAuthorization)); }); } } diff --git a/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityChangeOptions_Tests.cs b/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityChangeOptions_Tests.cs new file mode 100644 index 0000000000..3baf57ea2d --- /dev/null +++ b/modules/openiddict/test/Volo.Abp.OpenIddict.EntityFrameworkCore.Tests/Volo/Abp/OpenIddict/EntityFrameworkCore/AbpOpenIddictEntityChangeOptions_Tests.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict.Abstractions; +using Shouldly; +using Volo.Abp.OpenIddict.Applications; +using Volo.Abp.OpenIddict.Authorizations; +using Volo.Abp.OpenIddict.Tokens; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.OpenIddict.EntityFrameworkCore; + +/* + * OpenIddict tokens/authorizations reference their parent application/authorization via a + * foreign key. Creating a child (e.g. a token) must NOT bump the parent aggregate root's + * ConcurrencyStamp, otherwise concurrent creations under a shared parent collide with a + * DbUpdateConcurrencyException. AbpOpenIddictEntityFrameworkCoreModule excludes both parents + * from AbpEntityChangeOptions.IgnoredNavigationEntitySelectors. These tests guard that. + */ +public class AbpOpenIddictEntityChangeOptions_Tests : OpenIddictEntityFrameworkCoreTestBase +{ + private readonly AbpOpenIddictTestData _testData; + + public AbpOpenIddictEntityChangeOptions_Tests() + { + _testData = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task Creating_a_token_should_not_bump_the_referenced_application_stamp() + { + var before = await GetApplicationStampAsync(_testData.App1Id); + + await CreateTokenAsync(applicationId: _testData.App1Id, authorizationId: null, subject: "app-stamp-test"); + + var after = await GetApplicationStampAsync(_testData.App1Id); + after.ShouldBe(before); + } + + [Fact] + public async Task Creating_a_token_should_not_bump_the_referenced_authorization_stamp() + { + var before = await GetAuthorizationStampAsync(_testData.Authorization1Id); + + await CreateTokenAsync(applicationId: _testData.App1Id, authorizationId: _testData.Authorization1Id, subject: "authorization-stamp-test"); + + var after = await GetAuthorizationStampAsync(_testData.Authorization1Id); + after.ShouldBe(before); + } + + // Loads the referenced parents into the same DbContext (as the OpenIddict flow does) and + // then creates a token, so the navigation-change detection has a tracked parent to act on. + private async Task CreateTokenAsync(Guid applicationId, Guid? authorizationId, string subject) + { + using var scope = ServiceProvider.CreateScope(); + var uowManager = scope.ServiceProvider.GetRequiredService(); + using var uow = uowManager.Begin(new AbpUnitOfWorkOptions(), requiresNew: true); + + var applicationRepository = scope.ServiceProvider.GetRequiredService(); + var authorizationRepository = scope.ServiceProvider.GetRequiredService(); + var tokenStore = scope.ServiceProvider.GetRequiredService>(); + + await applicationRepository.GetAsync(applicationId); + if (authorizationId.HasValue) + { + await authorizationRepository.GetAsync(authorizationId.Value); + } + + await tokenStore.CreateAsync(new OpenIddictTokenModel + { + Id = Guid.NewGuid(), + ApplicationId = applicationId, + AuthorizationId = authorizationId, + Subject = subject, + Type = "access_token", + Status = OpenIddictConstants.Statuses.Valid, + Payload = "payload-" + subject + }, CancellationToken.None); + + await uow.CompleteAsync(); + } + + private async Task GetApplicationStampAsync(Guid id) + { + using var scope = ServiceProvider.CreateScope(); + var uowManager = scope.ServiceProvider.GetRequiredService(); + using var uow = uowManager.Begin(new AbpUnitOfWorkOptions(), requiresNew: true); + var stamp = (await scope.ServiceProvider.GetRequiredService().GetAsync(id)).ConcurrencyStamp; + await uow.CompleteAsync(); + return stamp; + } + + private async Task GetAuthorizationStampAsync(Guid id) + { + using var scope = ServiceProvider.CreateScope(); + var uowManager = scope.ServiceProvider.GetRequiredService(); + using var uow = uowManager.Begin(new AbpUnitOfWorkOptions(), requiresNew: true); + var stamp = (await scope.ServiceProvider.GetRequiredService().GetAsync(id)).ConcurrencyStamp; + await uow.CompleteAsync(); + return stamp; + } +}