From 99c2aaf17abc3d1b13b3843503f449b7a1ae82a7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 10 Oct 2025 09:52:57 +0200 Subject: [PATCH] Members: Forward port of fix for member lockout issue #16988 from PR #17007 for 16 (#20441) * Port PR #17007 * Update src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Security/IdentityMapDefinition.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs index 5032a2a871..c304634290 100644 --- a/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Security/IdentityMapDefinition.cs @@ -95,7 +95,7 @@ public class IdentityMapDefinition : IMapDefinition target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.UserDefaultLockoutTimeInMinutes); - target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; + target.LockoutEnd = source.IsLockedOut ? lockedOutUntil ?? DateTime.MaxValue : null; target.Kind = source.Kind; } @@ -114,16 +114,46 @@ public class IdentityMapDefinition : IMapDefinition target.IsApproved = source.IsApproved; target.SecurityStamp = source.SecurityStamp; DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes); - target.LockoutEnd = source.IsLockedOut ? (lockedOutUntil ?? DateTime.MaxValue).ToUniversalTime() : null; + target.LockoutEnd = GetLockoutEnd(source); + target.LastLockoutDateUtc = GetLastLockoutDateUtc(source); + target.CreatedDateUtc = EnsureUtcWithServerTime(source.CreateDate); target.Comments = source.Comments; - target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue - ? null - : source.LastLockoutDate?.ToUniversalTime(); - target.CreatedDateUtc = source.CreateDate.ToUniversalTime(); target.Key = source.Key; target.MemberTypeAlias = source.ContentTypeAlias; target.TwoFactorEnabled = _twoFactorLoginService.IsTwoFactorEnabledAsync(source.Key).GetAwaiter().GetResult(); // NB: same comments re AutoMapper as per BackOfficeUser } + + private DateTimeOffset? GetLockoutEnd(IMember source) + { + if (source.IsLockedOut is false) + { + return null; + } + + DateTime? lockedOutUntil = source.LastLockoutDate?.AddMinutes(_securitySettings.MemberDefaultLockoutTimeInMinutes); + if (lockedOutUntil.HasValue is false) + { + return DateTime.MaxValue; + } + + return EnsureUtcWithServerTime(lockedOutUntil.Value); + } + + private static DateTime? GetLastLockoutDateUtc(IMember source) + { + if (source.LastLockoutDate is null || source.LastLockoutDate == DateTime.MinValue) + { + return null; + } + + return EnsureUtcWithServerTime(source.LastLockoutDate.Value); + } + + private static DateTime EnsureUtcWithServerTime(DateTime date) => + + // We have a server time value here, but the Kind is UTC, so we can't use .ToUniversalTime() to convert to the UTC + // value that the LockoutEnd property expects. We need to create a DateTimeOffset with the correct offset. + DateTime.SpecifyKind(date, DateTimeKind.Local).ToUniversalTime(); }