fix(iam-service): add custom ResourceOwnerPasswordValidator for Duende password grant

- Created ResourceOwnerPasswordValidator using UserManager.CheckPasswordAsync
- Registered with .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
- Added comments explaining EF.Property pattern for DDD backing fields
This commit is contained in:
Ho Ngoc Hai
2026-02-28 03:12:31 +07:00
parent b9e5c4e31e
commit 68a6c4a81e
2 changed files with 105 additions and 0 deletions

View File

@@ -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<ApplicationUser>()
.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>()
.AddDeveloperSigningCredential(); // EN: Use certificate in production / VI: Dùng certificate trong production
// EN: Add JWT Bearer authentication for API endpoints using local IdentityServer

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<ResourceOwnerPasswordValidator> _logger;
public ResourceOwnerPasswordValidator(
UserManager<ApplicationUser> userManager,
ILogger<ResourceOwnerPasswordValidator> 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");
}
}
}