Open Closed

Concurrent logins cause AbpDbConcurrencyException updating LastSignInTime #10828


User avatar
0
devpayoff created

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?

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

2 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In ABP 10.5, IdentityUser implements optimistic concurrency via IHasConcurrencyStamp. That means any update to the user aggregate (including LastSignInTime) participates in concurrency control. So if two /connect/token requests load the same IdentityUser row and both call UserManager.UpdateAsync(user), one of them can legitimately fail with AbpDbConcurrencyException because the first update changes the ConcurrencyStamp and the second update now affects 0 rows.

    • So: Yes, updating LastSignInTime can 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:

      1. Catch and ignore AbpDbConcurrencyException only around the LastSignInTime update (i.e., keep issuing the token, treat timestamp update as best-effort).
      2. Retry-on-concurrency: reload the user and try updating LastSignInTime again once (or a small retry count). (This preserves the audit field update without failing logins.)
      3. Move LastSignInTime to 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.
      4. If your business rule is actually “don’t allow parallel sessions”, implement ABP’s concurrent login approach instead (separate concern from the timestamp update).
    • Conceptually: the concurrency exception is correct for the aggregate update, but it’s reasonable to not fail the authentication flow just because LastSignInTime couldn’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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    You're right, and no — updating LastSignInTime should not fail an otherwise valid login. This is a bug in the password-grant token flow: concurrent successful logins update LastSignInTime on 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 TokenController in 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 (the LastSignInTime update) 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 LastSignInTime concurrency 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)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.