From 7550929f50a6b15e5fe32fd52f62ee0a739f6d3d Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Tue, 13 Jan 2026 19:43:16 +0700 Subject: [PATCH] refactor(authentication): Update authorization schemes to use "Bearer" for consistency - Changed authorization schemes in AuthController, RolesController, and UsersController from JwtBearerDefaults.AuthenticationScheme to "Bearer" for uniformity across the application. - Added new endpoints in UsersController to retrieve user roles and permissions by user ID, enhancing user management capabilities. --- .../Controllers/AuthController.cs | 12 +- .../Controllers/RolesController.cs | 2 +- .../Controllers/UsersController.cs | 136 +++++++++++++++++- .../DependencyInjection.cs | 48 +++---- 4 files changed, 158 insertions(+), 40 deletions(-) diff --git a/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs b/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs index 911d2197..13724043 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/AuthController.cs @@ -86,7 +86,7 @@ public class AuthController : ControllerBase /// Cancellation token /// Result of password change operation [HttpPost("change-password")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Change password", Description = "Changes the password for the currently authenticated user. Requires current password verification.", @@ -125,7 +125,7 @@ public class AuthController : ControllerBase /// Cancellation token /// Result of logout operation [HttpPost("logout")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Logout", Description = "Logs out the current user and revokes all associated tokens.", @@ -219,7 +219,7 @@ public class AuthController : ControllerBase /// Cancellation token /// QR code and recovery codes [HttpPost("2fa/enable")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Enable 2FA", Description = "Initiates 2FA setup. Returns QR code and recovery codes. Must be verified with /2fa/verify.", @@ -264,7 +264,7 @@ public class AuthController : ControllerBase /// Cancellation token /// Result of 2FA verification [HttpPost("2fa/verify")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Verify 2FA code", Description = "Verifies the TOTP code and completes 2FA setup.", @@ -304,7 +304,7 @@ public class AuthController : ControllerBase /// Cancellation token /// Result of disabling 2FA [HttpPost("2fa/disable")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Disable 2FA", Description = "Disables 2FA for the current user. Requires verification with current 2FA code.", @@ -438,7 +438,7 @@ public class AuthController : ControllerBase /// Cancellation token /// List of linked providers [HttpGet("linked-accounts")] - [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] + [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = "Bearer")] [SwaggerOperation( Summary = "Get linked accounts", Description = "Returns list of external OAuth providers linked to current user's account.", diff --git a/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs b/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs index 43220a37..e118f6cd 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs @@ -17,7 +17,7 @@ namespace IamService.API.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/roles")] -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +[Authorize(AuthenticationSchemes = "Bearer")] [SwaggerTag("Role management endpoints - requires authentication")] public class RolesController : ControllerBase { diff --git a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs index f408af00..66ad5cd0 100644 --- a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs +++ b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs @@ -2,11 +2,13 @@ using Asp.Versioning; using MediatR; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; using IamService.API.Application.Common; using IamService.API.Application.Commands.Users; using IamService.API.Application.Queries.Users; +using IamService.Domain.AggregatesModel.UserAggregate; namespace IamService.API.Controllers; @@ -17,19 +19,22 @@ namespace IamService.API.Controllers; [ApiController] [ApiVersion("1.0")] [Route("api/v{version:apiVersion}/users")] -[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] +[Authorize(AuthenticationSchemes = "Bearer")] [SwaggerTag("User management endpoints - requires authentication")] public class UsersController : ControllerBase { private readonly IMediator _mediator; private readonly ILogger _logger; + private readonly UserManager _userManager; public UsersController( IMediator mediator, - ILogger logger) + ILogger logger, + UserManager userManager) { _mediator = mediator; _logger = logger; + _userManager = userManager; } /// @@ -238,6 +243,89 @@ public class UsersController : ControllerBase Roles = roles })); } + + /// + /// EN: Get user roles by user ID. + /// VI: Lấy danh sách roles của user theo ID. + /// + /// User ID + /// Cancellation token + /// List of user roles + [HttpGet("{id:guid}/roles")] + [SwaggerOperation( + Summary = "Get user roles", + Description = "Retrieves all roles assigned to a specific user.", + OperationId = "GetUserRoles")] + [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved user roles", typeof(ApiResponse))] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")] + [SwaggerResponse(StatusCodes.Status404NotFound, "User not found")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUserRoles( + [FromRoute, SwaggerParameter("User ID", Required = true)] Guid id, + CancellationToken cancellationToken = default) + { + var user = await _userManager.FindByIdAsync(id.ToString()); + if (user == null) + { + return NotFound(ApiResponse.Fail("USER_NOT_FOUND", $"User with ID {id} not found.")); + } + + var roles = await _userManager.GetRolesAsync(user); + + return Ok(ApiResponse.Ok(new UserRolesDto + { + UserId = id.ToString(), + Roles = roles + })); + } + + /// + /// EN: Get user permissions by user ID. + /// VI: Lấy danh sách permissions của user theo ID. + /// + /// User ID + /// Cancellation token + /// List of user permissions + [HttpGet("{id:guid}/permissions")] + [SwaggerOperation( + Summary = "Get user permissions", + Description = "Retrieves all permissions for a specific user (derived from roles).", + OperationId = "GetUserPermissions")] + [SwaggerResponse(StatusCodes.Status200OK, "Successfully retrieved user permissions", typeof(ApiResponse))] + [SwaggerResponse(StatusCodes.Status401Unauthorized, "Authentication required")] + [SwaggerResponse(StatusCodes.Status404NotFound, "User not found")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task GetUserPermissions( + [FromRoute, SwaggerParameter("User ID", Required = true)] Guid id, + CancellationToken cancellationToken = default) + { + var user = await _userManager.FindByIdAsync(id.ToString()); + if (user == null) + { + return NotFound(ApiResponse.Fail("USER_NOT_FOUND", $"User with ID {id} not found.")); + } + + var roles = await _userManager.GetRolesAsync(user); + var claims = await _userManager.GetClaimsAsync(user); + + // EN: Extract permissions from claims + // VI: Lấy permissions từ claims + var permissions = claims + .Where(c => c.Type == "permission") + .Select(c => c.Value) + .ToList(); + + return Ok(ApiResponse.Ok(new UserPermissionsDto + { + UserId = id.ToString(), + Roles = roles, + Permissions = permissions + })); + } } /// @@ -279,3 +367,47 @@ public class DeleteUserResult /// public string Message { get; set; } = string.Empty; } + +/// +/// EN: User roles response DTO. +/// VI: DTO response cho danh sách roles của user. +/// +public class UserRolesDto +{ + /// + /// EN: User ID. + /// VI: ID của user. + /// + public string UserId { get; set; } = string.Empty; + + /// + /// EN: List of role names assigned to the user. + /// VI: Danh sách tên roles được gán cho user. + /// + public IEnumerable Roles { get; set; } = Enumerable.Empty(); +} + +/// +/// EN: User permissions response DTO. +/// VI: DTO response cho danh sách permissions của user. +/// +public class UserPermissionsDto +{ + /// + /// EN: User ID. + /// VI: ID của user. + /// + public string UserId { get; set; } = string.Empty; + + /// + /// EN: List of role names assigned to the user. + /// VI: Danh sách tên roles được gán cho user. + /// + public IEnumerable Roles { get; set; } = Enumerable.Empty(); + + /// + /// EN: List of permission names assigned to the user. + /// VI: Danh sách tên permissions được gán cho user. + /// + public IEnumerable Permissions { get; set; } = Enumerable.Empty(); +} diff --git a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs index 83831aac..2b361727 100644 --- a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs +++ b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs @@ -113,40 +113,26 @@ public static class DependencyInjection .AddAspNetIdentity() .AddDeveloperSigningCredential(); // EN: Use certificate in production / VI: Dùng certificate trong production - // EN: Add JWT Bearer authentication for API endpoints - // VI: Thêm JWT Bearer authentication cho API endpoints + // EN: Add JWT Bearer authentication for API endpoints using local IdentityServer + // VI: Thêm JWT Bearer authentication cho API endpoints sử dụng IdentityServer cục bộ + // EN: AddLocalApi() allows JWT validation without external metadata calls (for self-hosted IdentityServer) + // VI: AddLocalApi() cho phép JWT validation mà không cần gọi metadata bên ngoài (cho self-hosted IdentityServer) services.AddAuthentication() - .AddJwtBearer(options => + .AddLocalApi("Bearer", options => { - // EN: Configure to validate tokens from local IdentityServer - // VI: Cấu hình để validate tokens từ IdentityServer cục bộ - options.Authority = configuration["IdentityServer:Authority"] ?? "https://localhost:5001"; - options.Audience = "iam-api"; - options.RequireHttpsMetadata = false; // EN: Set to true in production / VI: Đặt true trong production - - // EN: Disable metadata validation for testing/development - // VI: Tắt metadata validation cho testing/development - options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters - { - ValidateAudience = false, // EN: Disable for flexibility / VI: Tắt để linh hoạt - ValidateIssuer = false, // EN: Disable for testing / VI: Tắt cho testing - ValidateLifetime = true, - RequireSignedTokens = true, - ValidateIssuerSigningKey = true - }; - - // EN: Disable metadata address requirement if in testing environment - // VI: Tắt yêu cầu metadata address nếu trong testing environment - if (string.Equals(environmentName, "Testing", StringComparison.OrdinalIgnoreCase)) - { - options.MetadataAddress = string.Empty; - options.TokenValidationParameters.ValidateIssuerSigningKey = false; - options.TokenValidationParameters.RequireSignedTokens = false; - // EN: ASP.NET Core 8+ requires JsonWebToken, not JwtSecurityToken - // VI: ASP.NET Core 8+ yêu cầu JsonWebToken, không phải JwtSecurityToken - options.TokenValidationParameters.SignatureValidator = (token, parameters) => new Microsoft.IdentityModel.JsonWebTokens.JsonWebToken(token); - } + options.ExpectedScope = "openid"; }); + + // EN: Configure authorization policies + // VI: Cấu hình authorization policies + services.AddAuthorization(options => + { + options.AddPolicy("LocalApi", policy => + { + policy.AddAuthenticationSchemes("Bearer"); + policy.RequireAuthenticatedUser(); + }); + }); // EN: Register repositories // VI: Đăng ký repositories