// EN: Authentication controller with Duende IdentityServer integration. // VI: Controller xác thực với Duende IdentityServer integration. using System.Security.Claims; using Asp.Versioning; using Duende.IdentityServer; using Duende.IdentityServer.Events; using Duende.IdentityServer.Services; using MediatR; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using IamService.API.Application.Commands.Auth; using IamService.API.Application.Common; using IamService.Domain.AggregatesModel.UserAggregate; namespace IamService.API.Controllers; /// /// EN: Authentication controller with Duende IdentityServer integration. /// VI: Controller xác thực với Duende IdentityServer integration. /// [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/auth")] [SwaggerTag("Authentication endpoints - OAuth2/OIDC")] public class AuthController : ControllerBase { private readonly IMediator _mediator; private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IIdentityServerInteractionService _interaction; private readonly IEventService _events; private readonly ILogger _logger; public AuthController( IMediator mediator, UserManager userManager, SignInManager signInManager, IIdentityServerInteractionService interaction, IEventService events, ILogger logger) { _mediator = mediator; _userManager = userManager; _signInManager = signInManager; _interaction = interaction; _events = events; _logger = logger; } /// /// EN: Register a new user. /// VI: Đăng ký user mới. /// /// User registration data /// Cancellation token /// Registered user information [HttpPost("register")] [SwaggerOperation( Summary = "Register a new user", Description = "Creates a new user account with email and password.", OperationId = "RegisterUser")] [SwaggerResponse(StatusCodes.Status201Created, "User successfully registered", typeof(ApiResponse))] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid registration data")] [SwaggerResponse(StatusCodes.Status409Conflict, "User with this email already exists")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task Register( [FromBody, SwaggerRequestBody("User registration details", Required = true)] RegisterUserCommand command, CancellationToken cancellationToken) { var result = await _mediator.Send(command, cancellationToken); return CreatedAtAction(nameof(Register), new { id = result.UserId }, ApiResponse.Ok(result)); } /// /// EN: Login with email and password (Resource Owner Password Grant). /// VI: Đăng nhập với email và password (Resource Owner Password Grant). /// /// Login credentials /// Cancellation token /// Login result with token info [HttpPost("login")] [SwaggerOperation( Summary = "Login with credentials", Description = "Authenticates a user with email and password. For full OAuth2 flow, use /connect/token endpoint.", OperationId = "Login")] [SwaggerResponse(StatusCodes.Status200OK, "Login successful")] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid credentials")] [SwaggerResponse(StatusCodes.Status403Forbidden, "Account locked")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task Login( [FromBody, SwaggerRequestBody("Login credentials", Required = true)] LoginRequest request, CancellationToken cancellationToken) { var user = await _userManager.FindByEmailAsync(request.Email); if (user == null) { _logger.LogWarning("Login failed: user not found for {Email}", request.Email); return BadRequest(ApiResponse.Fail("INVALID_CREDENTIALS", "Invalid email or password.")); } // EN: Check password // VI: Kiểm tra password var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true); if (result.IsLockedOut) { _logger.LogWarning("Login failed: user {UserId} is locked out", user.Id); await _events.RaiseAsync(new UserLoginFailureEvent(user.Email!, "User locked out", clientId: null)); return StatusCode(StatusCodes.Status403Forbidden, ApiResponse.Fail("ACCOUNT_LOCKED", "Account is locked. Please try again later.")); } if (!result.Succeeded) { _logger.LogWarning("Login failed: invalid password for user {UserId}", user.Id); await _events.RaiseAsync(new UserLoginFailureEvent(user.Email!, "Invalid credentials", clientId: null)); return BadRequest(ApiResponse.Fail("INVALID_CREDENTIALS", "Invalid email or password.")); } // EN: Record login // VI: Ghi nhận login user.RecordLogin(); await _userManager.UpdateAsync(user); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Email!, user.Id.ToString(), user.FullName, clientId: null)); _logger.LogInformation("User {UserId} logged in successfully", user.Id); // EN: Note: Full token response requires OAuth2 flow via /connect/token // VI: Lưu ý: Response token đầy đủ yêu cầu OAuth2 flow qua /connect/token return Ok(ApiResponse.Ok(new LoginResponse { Success = true, Message = "Login successful. Use /connect/token with grant_type=password for access tokens.", UserId = user.Id, Email = user.Email!, FullName = user.FullName })); } /// /// EN: Change user password. /// VI: Đổi mật khẩu user. /// /// Change password request data /// Cancellation token /// Result of password change operation [HttpPost("change-password")] [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [SwaggerOperation( Summary = "Change password", Description = "Changes the password for the currently authenticated user. Requires current password verification.", OperationId = "ChangePassword")] [SwaggerResponse(StatusCodes.Status200OK, "Password changed successfully", typeof(ChangePasswordResponse))] [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request (current password incorrect)")] [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")] [ProducesResponseType(typeof(ChangePasswordResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task ChangePassword( [FromBody, SwaggerRequestBody("Password change data", Required = true)] ChangePasswordRequest request, CancellationToken cancellationToken) { var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) { return Unauthorized(); } var command = new ChangePasswordCommand(userId, request.CurrentPassword, request.NewPassword); var result = await _mediator.Send(command, cancellationToken); if (!result.Success) { return BadRequest(new ChangePasswordResponse { Success = false, Message = result.Message }); } return Ok(new ChangePasswordResponse { Success = true, Message = result.Message }); } /// /// EN: Logout user and revoke tokens. /// VI: Logout user và thu hồi tokens. /// /// Cancellation token /// Result of logout operation [HttpPost("logout")] [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] [SwaggerOperation( Summary = "Logout", Description = "Logs out the current user and revokes all associated tokens.", OperationId = "Logout")] [SwaggerResponse(StatusCodes.Status200OK, "User logged out successfully", typeof(LogoutResponse))] [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")] [ProducesResponseType(typeof(LogoutResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Logout(CancellationToken cancellationToken) { var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) { return Unauthorized(); } var command = new LogoutCommand(userId); var result = await _mediator.Send(command, cancellationToken); await _signInManager.SignOutAsync(); return Ok(new LogoutResponse { Success = result.Success, Message = result.Message }); } } #region Request/Response Models /// /// EN: Login request body. /// VI: Request body đăng nhập. /// public class LoginRequest { /// /// EN: User email. /// VI: Email người dùng. /// /// user@example.com public string Email { get; set; } = string.Empty; /// /// EN: User password. /// VI: Mật khẩu người dùng. /// /// Password123! public string Password { get; set; } = string.Empty; } /// /// EN: Login response. /// VI: Response đăng nhập. /// public class LoginResponse { /// /// EN: Whether the login was successful. /// VI: Đăng nhập có thành công không. /// public bool Success { get; set; } /// /// EN: Result message. /// VI: Thông điệp kết quả. /// public string Message { get; set; } = string.Empty; /// /// EN: User ID. /// VI: ID người dùng. /// public Guid UserId { get; set; } /// /// EN: User email. /// VI: Email người dùng. /// public string Email { get; set; } = string.Empty; /// /// EN: User full name. /// VI: Tên đầy đủ người dùng. /// public string FullName { get; set; } = string.Empty; } /// /// EN: Request body for changing password. /// VI: Request body để đổi mật khẩu. /// public class ChangePasswordRequest { /// /// EN: Current password. /// VI: Mật khẩu hiện tại. /// /// OldPassword123! public string CurrentPassword { get; set; } = string.Empty; /// /// EN: New password. /// VI: Mật khẩu mới. /// /// NewPassword456! public string NewPassword { get; set; } = string.Empty; } /// /// EN: Response for change password operation. /// VI: Response cho thao tác đổi mật khẩu. /// public class ChangePasswordResponse { /// /// EN: Whether the operation was successful. /// VI: Thao tác có thành công không. /// public bool Success { get; set; } /// /// EN: Result message. /// VI: Thông điệp kết quả. /// public string Message { get; set; } = string.Empty; } /// /// EN: Response for logout operation. /// VI: Response cho thao tác logout. /// public class LogoutResponse { /// /// EN: Whether the operation was successful. /// VI: Thao tác có thành công không. /// public bool Success { get; set; } /// /// EN: Result message. /// VI: Thông điệp kết quả. /// public string Message { get; set; } = string.Empty; } #endregion