Browse Source

Update user's last sign-in time in a separate unit of work

pull/25905/head
maliming 2 months ago
parent
commit
cb7667557b
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 60
      modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs
  2. 32
      modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityUserRepository_Tests.cs
  3. 24
      modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs
  4. 3
      modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs
  5. 8
      modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs

60
modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs

@ -36,6 +36,7 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
protected IOptions<AbpMultiTenancyOptions> MultiTenancyOptions { get; } protected IOptions<AbpMultiTenancyOptions> MultiTenancyOptions { get; }
protected ICurrentTenant CurrentTenant { get; } protected ICurrentTenant CurrentTenant { get; }
protected IDataFilter DataFilter { get; } protected IDataFilter DataFilter { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
public IdentityUserManager( public IdentityUserManager(
IdentityUserStore store, IdentityUserStore store,
@ -57,7 +58,8 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache, IDistributedCache<AbpDynamicClaimCacheItem> dynamicClaimCache,
IOptions<AbpMultiTenancyOptions> multiTenancyOptions, IOptions<AbpMultiTenancyOptions> multiTenancyOptions,
ICurrentTenant currentTenant, ICurrentTenant currentTenant,
IDataFilter dataFilter) IDataFilter dataFilter,
IUnitOfWorkManager unitOfWorkManager)
: base( : base(
store, store,
optionsAccessor, optionsAccessor,
@ -79,6 +81,7 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
MultiTenancyOptions = multiTenancyOptions; MultiTenancyOptions = multiTenancyOptions;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
DataFilter = dataFilter; DataFilter = dataFilter;
UnitOfWorkManager = unitOfWorkManager;
CancellationTokenProvider = cancellationTokenProvider; CancellationTokenProvider = cancellationTokenProvider;
} }
@ -167,6 +170,61 @@ public class IdentityUserManager : UserManager<IdentityUser>, IDomainService
return user; return user;
} }
public virtual async Task UpdateLastSignInTimeAsync(Guid id, DateTimeOffset? lastSignInTime = null)
{
var time = lastSignInTime ?? DateTimeOffset.UtcNow;
var currentUow = UnitOfWorkManager.Current;
if (currentUow != null)
{
// The current unit of work may hold uncommitted changes of the same user (e.g. a new
// registration or a lockout counter reset), so update the time after it completes.
var tenantId = CurrentTenant.Id;
currentUow.OnCompleted(async () =>
{
using (CurrentTenant.Change(tenantId))
{
await TryUpdateLastSignInTimeAsync(id, time);
}
});
return;
}
await TryUpdateLastSignInTimeAsync(id, time);
}
protected virtual async Task TryUpdateLastSignInTimeAsync(Guid id, DateTimeOffset lastSignInTime)
{
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 current operation.
using (var uow = UnitOfWorkManager.Begin(requiresNew: true))
{
var user = await Store.FindByIdAsync(id.ToString(), CancellationToken);
if (user == null || user.LastSignInTime >= lastSignInTime)
{
return;
}
user.SetLastSignInTime(lastSignInTime);
var result = await UpdateAsync(user);
if (result.Succeeded)
{
await uow.CompleteAsync();
}
}
}
catch (Exception e)
{
// This is a best-effort update. The user may be updated concurrently
// by another login or any other operation. Ignore the failure.
Logger.LogException(e);
}
}
public virtual async Task<IdentityResult> SetRolesAsync([NotNull] IdentityUser user, public virtual async Task<IdentityResult> SetRolesAsync([NotNull] IdentityUser user,
[NotNull] IEnumerable<string> roleNames) [NotNull] IEnumerable<string> roleNames)
{ {

32
modules/identity/test/Volo.Abp.Identity.EntityFrameworkCore.Tests/Volo/Abp/Identity/EntityFrameworkCore/IdentityUserRepository_Tests.cs

@ -1,6 +1,36 @@
namespace Volo.Abp.Identity.EntityFrameworkCore; using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.Identity.EntityFrameworkCore;
public class IdentityUserRepository_Tests : IdentityUserRepository_Tests<AbpIdentityEntityFrameworkCoreTestModule> public class IdentityUserRepository_Tests : IdentityUserRepository_Tests<AbpIdentityEntityFrameworkCoreTestModule>
{ {
[Fact]
public async Task UpdateLastSignInTimeAsync_Should_Be_Deferred_In_A_Transactional_UnitOfWork()
{
var userManager = ServiceProvider.GetRequiredService<IdentityUserManager>();
var unitOfWorkManager = ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var user = new IdentityUser(Guid.NewGuid(), "bob.lee", "bob.lee@abp.io");
using (var uow = unitOfWorkManager.Begin(new AbpUnitOfWorkOptions
{
IsTransactional = true
}, requiresNew: true))
{
(await userManager.CreateAsync(user)).CheckErrors();
await userManager.UpdateLastSignInTimeAsync(user.Id);
await uow.CompleteAsync();
}
var createdUser = await UserRepository.FindAsync(user.Id);
createdUser.ShouldNotBeNull();
createdUser.LastSignInTime.ShouldNotBeNull();
}
} }

24
modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/IdentityUserRepository_Tests.cs

@ -302,6 +302,30 @@ public abstract class IdentityUserRepository_Tests<TStartupModule> : AbpIdentity
ou112Users.ShouldContain(x => x.UserName == "neo"); ou112Users.ShouldContain(x => x.UserName == "neo");
} }
[Fact]
public async Task UpdateLastSignInTimeAsync()
{
var userManager = ServiceProvider.GetRequiredService<IdentityUserManager>();
var john = await UserRepository.FindByNormalizedUserNameAsync(LookupNormalizer.NormalizeName("john.nash"));
john.LastSignInTime.ShouldBeNull();
var lastSignInTime = DateTimeOffset.UtcNow;
await userManager.UpdateLastSignInTimeAsync(john.Id, lastSignInTime);
john = await UserRepository.FindByNormalizedUserNameAsync(LookupNormalizer.NormalizeName("john.nash"));
john.LastSignInTime.ShouldNotBeNull();
john.LastSignInTime.Value.ShouldBe(lastSignInTime, TimeSpan.FromSeconds(1));
// An older time (e.g. from a concurrent login that lost the race) should not overwrite a newer one.
await userManager.UpdateLastSignInTimeAsync(john.Id, lastSignInTime.AddMinutes(-30));
john = await UserRepository.FindByNormalizedUserNameAsync(LookupNormalizer.NormalizeName("john.nash"));
john.LastSignInTime!.Value.ShouldBe(lastSignInTime, TimeSpan.FromSeconds(1));
await userManager.UpdateLastSignInTimeAsync(Guid.NewGuid());
}
[Fact] [Fact]
public async Task FindByPasskeyIdAsync() public async Task FindByPasskeyIdAsync()

3
modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AspNetIdentity/AbpResourceOwnerPasswordValidator.cs

@ -322,8 +322,7 @@ public class AbpResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
additionalClaims.ToArray() additionalClaims.ToArray()
); );
user.SetLastSignInTime(DateTimeOffset.UtcNow); await UserManager.UpdateLastSignInTimeAsync(user.Id);
await UserManager.UpdateAsync(user);
await IdentitySecurityLogManager.SaveAsync( await IdentitySecurityLogManager.SaveAsync(
new IdentitySecurityLogContext new IdentitySecurityLogContext

8
modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs

@ -416,12 +416,16 @@ public partial class TokenController
} }
); );
user.SetLastSignInTime(DateTimeOffset.UtcNow); await UpdateUserLastSignInTimeAsync(user);
await UserManager.UpdateAsync(user);
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
} }
protected virtual async Task UpdateUserLastSignInTimeAsync(IdentityUser user)
{
await UserManager.UpdateLastSignInTimeAsync(user.Id);
}
protected virtual async Task<bool> IsTfaEnabledAsync(IdentityUser user) protected virtual async Task<bool> IsTfaEnabledAsync(IdentityUser user)
{ {
return UserManager.SupportsUserTwoFactor && return UserManager.SupportsUserTwoFactor &&

Loading…
Cancel
Save