diff --git a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs index b9b0f81c..5c0143c7 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs @@ -112,6 +112,12 @@ public static class DependencyInjection options.IssuerUri = issuerUri; } + // EN: Disable automatic key management (requires Business/Enterprise license) + // EN: Using AddDeveloperSigningCredential() instead for development + // VI: Tắt quản lý key tự động (yêu cầu license Business/Enterprise) + // VI: Sử dụng AddDeveloperSigningCredential() cho môi trường phát triển + options.KeyManagement.Enabled = false; + // EN: Events for logging // VI: Events để logging options.Events.RaiseErrorEvents = true; @@ -128,6 +134,7 @@ public static class DependencyInjection .AddInMemoryApiResources(Config.ApiResources) .AddInMemoryClients(Config.Clients) .AddAspNetIdentity() + .AddResourceOwnerValidator() .AddDeveloperSigningCredential(); // EN: Use certificate in production / VI: Dùng certificate trong production // EN: Add JWT Bearer authentication for API endpoints using local IdentityServer diff --git a/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/ResourceOwnerPasswordValidator.cs b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/ResourceOwnerPasswordValidator.cs new file mode 100644 index 00000000..ff2c3b1b --- /dev/null +++ b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/ResourceOwnerPasswordValidator.cs @@ -0,0 +1,98 @@ +// EN: Custom Resource Owner Password Validator for Duende IdentityServer. +// VI: Custom Resource Owner Password Validator cho Duende IdentityServer. + +using Duende.IdentityServer.Models; +using Duende.IdentityServer.Validation; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using IamService.Domain.AggregatesModel.UserAggregate; + +namespace IamService.Infrastructure.IdentityServer; + +/// +/// EN: Custom Resource Owner Password Validator using ASP.NET Identity UserManager. +/// EN: Avoids SignInManager cookie-based validation that can deadlock in token endpoint. +/// VI: Custom Resource Owner Password Validator sử dụng ASP.NET Identity UserManager. +/// VI: Tránh SignInManager dựa trên cookie có thể gây deadlock trong token endpoint. +/// +public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public ResourceOwnerPasswordValidator( + UserManager userManager, + ILogger logger) + { + _userManager = userManager; + _logger = logger; + } + + public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) + { + try + { + // EN: Find user by username (which is email) + // VI: Tìm user bằng username (tức là email) + var user = await _userManager.FindByNameAsync(context.UserName); + + if (user == null) + { + _logger.LogWarning("User not found: {Username}", context.UserName); + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "Invalid username or password"); + return; + } + + // EN: Check if account is locked out + // VI: Kiểm tra tài khoản có bị khóa không + if (await _userManager.IsLockedOutAsync(user)) + { + _logger.LogWarning("User account is locked: {Username}", context.UserName); + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "Account is locked. Please try again later."); + return; + } + + // EN: Validate password directly via UserManager (avoids cookie-based SignInManager) + // VI: Xác thực password trực tiếp qua UserManager (tránh SignInManager dựa trên cookie) + var passwordValid = await _userManager.CheckPasswordAsync(user, context.Password); + + if (!passwordValid) + { + _logger.LogWarning("Invalid password for user: {Username}", context.UserName); + await _userManager.AccessFailedAsync(user); + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "Invalid username or password"); + return; + } + + // EN: Reset failed access count on successful login + // VI: Reset số lần truy cập thất bại khi đăng nhập thành công + await _userManager.ResetAccessFailedCountAsync(user); + + // EN: Record login + // VI: Ghi nhận đăng nhập + user.RecordLogin(); + + _logger.LogInformation("User {Username} authenticated successfully via password grant", context.UserName); + + // EN: Return successful result with subject claim + // VI: Trả về kết quả thành công với subject claim + context.Result = new GrantValidationResult( + subject: user.Id.ToString(), + authenticationMethod: "password", + claims: null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error validating resource owner password for {Username}", context.UserName); + context.Result = new GrantValidationResult( + TokenRequestErrors.InvalidGrant, + "An error occurred during authentication"); + } + } +}