ABP version: 10.5 Database: PostgreSQL / EF Core Authentication: OpenIddict password grant
After upgrading from ABP 8, concurrent /connect/token requests for the same user cause:
AbpDbConcurrencyException: The database operation was expected to affect 1 row, but affected 0 rows.
IdentityUser {Id: ...} Modified at IdentityUserStore.UpdateAsync(...)
In ABP 10.5, TokenController.SetSuccessResultAsync updates the login timestamp using:
user.SetLastSignInTime(DateTimeOffset.UtcNow); await UserManager.UpdateAsync(user);
Two concurrent logins load the same ConcurrencyStamp. The first update succeeds, while the second fails.
Should updating LastSignInTime cause a concurrency failure for an otherwise valid login?
2 Answer(s)
-
0
In ABP 10.5,
IdentityUserimplements optimistic concurrency viaIHasConcurrencyStamp. That means any update to the user aggregate (includingLastSignInTime) participates in concurrency control. So if two/connect/tokenrequests load the sameIdentityUserrow and both callUserManager.UpdateAsync(user), one of them can legitimately fail withAbpDbConcurrencyExceptionbecause the first update changes theConcurrencyStampand the second update now affects0rows.So: Yes, updating
LastSignInTimecan cause a concurrency failure under concurrent logins with the current behavior, even if the credentials are valid.Whether this should fail the login is more of a product/behavior decision:
- ABP’s concurrency system is working as designed (it prevents lost updates).
- But in a token endpoint, updating “last sign-in” is non-critical telemetry, so letting it break a valid login is typically undesirable.
Practical options (server-side) to avoid rejecting a valid login because of this non-critical update:
- Catch and ignore
AbpDbConcurrencyExceptiononly around theLastSignInTimeupdate (i.e., keep issuing the token, treat timestamp update as best-effort). - Retry-on-concurrency: reload the user and try updating
LastSignInTimeagain once (or a small retry count). (This preserves the audit field update without failing logins.) - Move
LastSignInTimeto a separate table/entity (or update it with a direct SQL update that doesn’t use the concurrency stamp), so it doesn’t contend with user aggregate concurrency. - If your business rule is actually “don’t allow parallel sessions”, implement ABP’s concurrent login approach instead (separate concern from the timestamp update).
- Catch and ignore
Conceptually: the concurrency exception is correct for the aggregate update, but it’s reasonable to not fail the authentication flow just because
LastSignInTimecouldn’t be written due to a race.
Sources (verified):
- https://abp.io/docs/10.5/framework/infrastructure/concurrency-check
- https://abp.io/support/questions/7409
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
You're right, and no — updating
LastSignInTimeshould not fail an otherwise valid login. This is a bug in the password-grant token flow: concurrent successful logins updateLastSignInTimeon the same user, and the resulting concurrency conflict can fail otherwise valid token requests. We'll fix this in the framework.Until the fix is released, you can override the
TokenControllerin your AuthServer project so a concurrency conflict while updating the timestamp does not fail the token request. The method body is copied from the base implementation; only the last part (theLastSignInTimeupdate) is changed to run in a separate unit of work with a freshly loaded user. Replace the namespace with your AuthServer project's namespace:using System; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using Volo.Abp.Data; using Volo.Abp.DependencyInjection; using Volo.Abp.Identity; using Volo.Abp.OpenIddict; using Volo.Abp.OpenIddict.Controllers; using Volo.Abp.Security.Claims; using Volo.Abp.Uow; namespace MyCompanyName.MyProjectName; [Dependency(ReplaceServices = true)] [ExposeServices(typeof(TokenController))] public class MyTokenController : TokenController { protected override async Task<IActionResult> SetSuccessResultAsync(OpenIddictRequest request, IdentityUser user) { // Clear the dynamic claims cache. await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); // Create a new ClaimsPrincipal containing the claims that // will be used to create an id_token, a token or a code. var principal = await SignInManager.CreateUserPrincipalAsync(user); var rememberMe = request.GetParameter("RememberMe").ToString(); if (!rememberMe.IsNullOrWhiteSpace() && bool.TryParse(rememberMe, out var rememberMeValue) && rememberMeValue) { var claim = new Claim(AbpClaimTypes.RememberMe, true.ToString()).SetDestinations(OpenIddictConstants.Destinations.AccessToken); principal.Identities.FirstOrDefault()?.AddClaim(claim); } principal.SetScopes(request.GetScopes()); principal.SetResources(await GetResourcesAsync(request.GetScopes())); await OpenIddictClaimsPrincipalManager.HandleAsync(request, principal); await IdentitySecurityLogManager.SaveAsync( new IdentitySecurityLogContext { Identity = OpenIddictSecurityLogIdentityConsts.OpenIddict, Action = OpenIddictSecurityLogActionConsts.LoginSucceeded, UserName = request.Username, ClientId = request.ClientId } ); try { // Update the last sign-in time in a separate unit of work with a freshly // loaded user, so a concurrency conflict can't fail the token request. using (var uow = UnitOfWorkManager.Begin(new AbpUnitOfWorkOptions(), requiresNew: true)) { var freshUser = await UserManager.GetByIdAsync(user.Id); freshUser.SetLastSignInTime(DateTimeOffset.UtcNow); var result = await UserManager.UpdateAsync(freshUser); if (result.Succeeded) { await uow.CompleteAsync(); } } } catch (AbpDbConcurrencyException) { // Ignore a concurrency conflict in the timestamp update. } return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } }With this override, concurrent valid password-grant requests are no longer failed by the
LastSignInTimeconcurrency conflict. The request that loses the timestamp race skips that update and can still receive its token.Once a framework version containing the fix is released, you can remove the override.
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)