using Microsoft.AspNetCore.Identity; using MS.Microservice.Core.Security.Cryptology; using MS.Microservice.Domain.Aggregates.IdentityModel; using MS.Microservice.Domain.Services.Interfaces; using System.Security.Cryptography; using System.Text; namespace MS.Microservice.Web.Application.Identity; public interface IUserPasswordService { string HashPassword(User user, string password); Task VerifyAndUpgradeAsync( User user, string providedPassword, CancellationToken cancellationToken = default); } public sealed class UserPasswordService( IPasswordHasher passwordHasher, IUserDomainService userDomainService) : IUserPasswordService { private readonly IPasswordHasher _passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher)); private readonly IUserDomainService _userDomainService = userDomainService ?? throw new ArgumentNullException(nameof(userDomainService)); public string HashPassword(User user, string password) { ArgumentNullException.ThrowIfNull(user); ArgumentException.ThrowIfNullOrWhiteSpace(password); return _passwordHasher.HashPassword(user, password); } public async Task VerifyAndUpgradeAsync( User user, string providedPassword, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(user); if (string.IsNullOrEmpty(providedPassword) || string.IsNullOrWhiteSpace(user.Password)) { return false; } var verificationResult = VerifyModernPassword(user, user.Password, providedPassword); if (verificationResult == PasswordVerificationResult.Failed && VerifyLegacyPassword(user, providedPassword)) { verificationResult = PasswordVerificationResult.SuccessRehashNeeded; } if (verificationResult == PasswordVerificationResult.Failed) { return false; } if (verificationResult == PasswordVerificationResult.Success) { return true; } var upgradedHash = HashPassword(user, providedPassword); return await _userDomainService.UpdatePasswordHashAsync(user, upgradedHash, cancellationToken); } private PasswordVerificationResult VerifyModernPassword( User user, string passwordHash, string providedPassword) { try { return _passwordHasher.VerifyHashedPassword(user, passwordHash, providedPassword); } catch (FormatException) { return PasswordVerificationResult.Failed; } } private static bool VerifyLegacyPassword(User user, string providedPassword) { if (string.IsNullOrEmpty(user.Salt) || string.Equals(user.Salt, User.ModernPasswordSaltMarker, StringComparison.Ordinal) || string.IsNullOrEmpty(user.Password)) { return false; } var expectedHash = CryptologyHelper.HmacSha256(providedPassword + user.Salt); var expectedBytes = Encoding.UTF8.GetBytes(expectedHash); var actualBytes = Encoding.UTF8.GetBytes(user.Password); return expectedBytes.Length == actualBytes.Length && CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes); } }