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:
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user