diff --git a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/LogoutCommandHandler.cs b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/LogoutCommandHandler.cs
index 655a56b3..d18b483e 100644
--- a/services/iam-service-net/src/IamService.API/Application/Commands/Auth/LogoutCommandHandler.cs
+++ b/services/iam-service-net/src/IamService.API/Application/Commands/Auth/LogoutCommandHandler.cs
@@ -1,27 +1,29 @@
+// EN: Handler for LogoutCommand - logs out the user.
+// VI: Handler cho LogoutCommand - logout user.
+
using MediatR;
using Microsoft.AspNetCore.Identity;
-using OpenIddict.Abstractions;
using IamService.Domain.AggregatesModel.UserAggregate;
namespace IamService.API.Application.Commands.Auth;
///
-/// EN: Handler for LogoutCommand - revokes all tokens for the user.
-/// VI: Handler cho LogoutCommand - thu hồi tất cả tokens của user.
+/// EN: Handler for LogoutCommand - logs out the user.
+/// VI: Handler cho LogoutCommand - logout user.
///
public class LogoutCommandHandler : IRequestHandler
{
private readonly UserManager _userManager;
- private readonly IOpenIddictTokenManager _tokenManager;
+ private readonly SignInManager _signInManager;
private readonly ILogger _logger;
public LogoutCommandHandler(
UserManager userManager,
- IOpenIddictTokenManager tokenManager,
+ SignInManager signInManager,
ILogger logger)
{
_userManager = userManager;
- _tokenManager = tokenManager;
+ _signInManager = signInManager;
_logger = logger;
}
@@ -41,24 +43,17 @@ public class LogoutCommandHandler : IRequestHandler
-/// EN: Authentication controller with OpenIddict OAuth2/OIDC endpoints.
-/// VI: Controller xác thực với OpenIddict OAuth2/OIDC endpoints.
+/// EN: Authentication controller with Duende IdentityServer integration.
+/// VI: Controller xác thực với Duende IdentityServer integration.
///
[ApiController]
[ApiVersion("1.0")]
@@ -29,17 +31,23 @@ 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;
}
@@ -69,219 +77,73 @@ public class AuthController : ControllerBase
return CreatedAtAction(nameof(Register), new { id = result.UserId }, ApiResponse.Ok(result));
}
-
///
- /// EN: OAuth2 Token endpoint - supports password, refresh_token, and client_credentials grants.
- /// VI: OAuth2 Token endpoint - hỗ trợ password, refresh_token, và client_credentials grants.
+ /// EN: Login with email and password (Resource Owner Password Grant).
+ /// VI: Đăng nhập với email và password (Resource Owner Password Grant).
///
- ///
- /// **Password Grant (Login):**
- /// ```
- /// POST /connect/token
- /// Content-Type: application/x-www-form-urlencoded
- ///
- /// grant_type=password&username=user@example.com&password=YourPassword&scope=openid profile email roles api
- /// ```
- ///
- /// **Refresh Token Grant:**
- /// ```
- /// POST /connect/token
- /// Content-Type: application/x-www-form-urlencoded
- ///
- /// grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN
- /// ```
- ///
- /// **Client Credentials Grant:**
- /// ```
- /// POST /connect/token
- /// Content-Type: application/x-www-form-urlencoded
- ///
- /// grant_type=client_credentials&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope=api
- /// ```
- ///
- /// OAuth2 token response with access_token, refresh_token, expires_in
- [HttpPost("~/connect/token")]
- [Consumes("application/x-www-form-urlencoded")]
- [Produces("application/json")]
+ /// Login credentials
+ /// Cancellation token
+ /// Login result with token info
+ [HttpPost("login")]
[SwaggerOperation(
- Summary = "OAuth2 Token Endpoint",
- Description = "Exchanges credentials for access tokens. Supports password, refresh_token, and client_credentials grant types.",
- OperationId = "GetToken",
- Tags = new[] { "Authentication" })]
- [SwaggerResponse(StatusCodes.Status200OK, "Token issued successfully", typeof(TokenResponse))]
- [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request (missing parameters)")]
- [SwaggerResponse(StatusCodes.Status401Unauthorized, "Invalid credentials or token")]
- [SwaggerResponse(StatusCodes.Status403Forbidden, "Account locked or unsupported grant type")]
- [ProducesResponseType(typeof(TokenResponse), StatusCodes.Status200OK)]
+ 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.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
- public async Task Exchange()
+ public async Task Login(
+ [FromBody, SwaggerRequestBody("Login credentials", Required = true)] LoginRequest request,
+ CancellationToken cancellationToken)
{
- var request = HttpContext.GetOpenIddictServerRequest()
- ?? throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
-
- if (request.IsPasswordGrantType())
- {
- return await HandlePasswordGrantAsync(request);
- }
-
- if (request.IsRefreshTokenGrantType())
- {
- return await HandleRefreshTokenGrantAsync();
- }
-
- if (request.IsClientCredentialsGrantType())
- {
- return await HandleClientCredentialsGrantAsync(request);
- }
-
- _logger.LogWarning("Unsupported grant type: {GrantType}", request.GrantType);
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnsupportedGrantType,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The specified grant type is not supported."
- }));
- }
-
- private async Task HandlePasswordGrantAsync(OpenIddictRequest request)
- {
- var user = await _userManager.FindByEmailAsync(request.Username!);
+ var user = await _userManager.FindByEmailAsync(request.Email);
if (user == null)
{
- _logger.LogWarning("Login failed: user not found for {Email}", request.Username);
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Invalid email or password."
- }));
+ _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);
+ var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true);
if (result.IsLockedOut)
{
_logger.LogWarning("Login failed: user {UserId} is locked out", user.Id);
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Account is locked. Please try again later."
- }));
+ 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);
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "Invalid email or password."
- }));
+ await _events.RaiseAsync(new UserLoginFailureEvent(user.Email!, "Invalid credentials", clientId: null));
+ return BadRequest(ApiResponse.Fail("INVALID_CREDENTIALS", "Invalid email or password."));
}
- // EN: Record login and create claims
- // VI: Ghi nhận login và tạo claims
+ // EN: Record login
+ // VI: Ghi nhận login
user.RecordLogin();
+ await _userManager.UpdateAsync(user);
- var identity = new ClaimsIdentity(
- authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- nameType: Claims.Name,
- roleType: Claims.Role);
-
- // EN: Add claims
- // VI: Thêm claims
- identity.SetClaim(Claims.Subject, user.Id.ToString())
- .SetClaim(Claims.Email, user.Email)
- .SetClaim(Claims.Name, user.FullName)
- .SetClaim("first_name", user.FirstName)
- .SetClaim("last_name", user.LastName);
-
- // EN: Add roles to claims
- // VI: Thêm roles vào claims
- var roles = await _userManager.GetRolesAsync(user);
- identity.SetClaims(Claims.Role, [.. roles]);
-
- // EN: Set destinations for claims
- // VI: Set destinations cho claims
- identity.SetDestinations(GetDestinations);
-
- var principal = new ClaimsPrincipal(identity);
-
- // EN: Set scopes
- // VI: Set scopes
- principal.SetScopes(request.GetScopes());
+ await _events.RaiseAsync(new UserLoginSuccessEvent(user.Email!, user.Id.ToString(), user.FullName, clientId: null));
_logger.LogInformation("User {UserId} logged in successfully", user.Id);
- return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
- }
-
- private async Task HandleRefreshTokenGrantAsync()
- {
- var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
- var userId = result.Principal?.GetClaim(Claims.Subject);
-
- if (string.IsNullOrEmpty(userId))
+ // 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
{
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The refresh token is no longer valid."
- }));
- }
-
- var user = await _userManager.FindByIdAsync(userId);
- if (user == null)
- {
- return Forbid(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties(new Dictionary
- {
- [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
- [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user no longer exists."
- }));
- }
-
- // EN: Recreate principal with updated claims
- // VI: Tạo lại principal với claims đã cập nhật
- var identity = new ClaimsIdentity(result.Principal!.Claims,
- authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- nameType: Claims.Name,
- roleType: Claims.Role);
-
- identity.SetDestinations(GetDestinations);
-
- return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
- }
-
- private Task HandleClientCredentialsGrantAsync(OpenIddictRequest request)
- {
- var identity = new ClaimsIdentity(
- authenticationType: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- nameType: Claims.Name,
- roleType: Claims.Role);
-
- identity.SetClaim(Claims.Subject, request.ClientId);
-
- identity.SetDestinations(GetDestinations);
-
- var principal = new ClaimsPrincipal(identity);
- principal.SetScopes(request.GetScopes());
-
- return Task.FromResult(
- SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
+ Success = true,
+ Message = "Login successful. Use /connect/token with grant_type=password for access tokens.",
+ UserId = user.Id,
+ Email = user.Email!,
+ FullName = user.FullName
+ }));
}
///
@@ -292,7 +154,7 @@ public class AuthController : ControllerBase
/// Cancellation token
/// Result of password change operation
[HttpPost("change-password")]
- [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+ [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[SwaggerOperation(
Summary = "Change password",
Description = "Changes the password for the currently authenticated user. Requires current password verification.",
@@ -307,7 +169,7 @@ public class AuthController : ControllerBase
[FromBody, SwaggerRequestBody("Password change data", Required = true)] ChangePasswordRequest request,
CancellationToken cancellationToken)
{
- var userIdClaim = User.FindFirst("sub")?.Value;
+ var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
return Unauthorized();
@@ -331,7 +193,7 @@ public class AuthController : ControllerBase
/// Cancellation token
/// Result of logout operation
[HttpPost("logout")]
- [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+ [Microsoft.AspNetCore.Authorization.Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[SwaggerOperation(
Summary = "Logout",
Description = "Logs out the current user and revokes all associated tokens.",
@@ -342,7 +204,7 @@ public class AuthController : ControllerBase
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task Logout(CancellationToken cancellationToken)
{
- var userIdClaim = User.FindFirst("sub")?.Value;
+ var userIdClaim = User.FindFirst("sub")?.Value ?? User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
{
return Unauthorized();
@@ -351,30 +213,70 @@ public class AuthController : ControllerBase
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 });
}
+}
- private static IEnumerable GetDestinations(Claim claim)
- {
- switch (claim.Type)
- {
- case Claims.Name or Claims.Email:
- yield return Destinations.AccessToken;
- if (claim.Subject?.HasScope(Scopes.Profile) == true)
- yield return Destinations.IdentityToken;
- yield break;
+#region Request/Response Models
- case Claims.Role:
- yield return Destinations.AccessToken;
- if (claim.Subject?.HasScope(Scopes.Roles) == true)
- yield return Destinations.IdentityToken;
- yield break;
+///
+/// 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;
- default:
- yield return Destinations.AccessToken;
- yield break;
- }
- }
+ ///
+ /// 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;
}
///
@@ -435,3 +337,5 @@ public class LogoutResponse
///
public string Message { get; set; } = string.Empty;
}
+
+#endregion
diff --git a/services/iam-service-net/src/IamService.API/Controllers/AuthorizationController.cs b/services/iam-service-net/src/IamService.API/Controllers/AuthorizationController.cs
deleted file mode 100644
index 89ff76de..00000000
--- a/services/iam-service-net/src/IamService.API/Controllers/AuthorizationController.cs
+++ /dev/null
@@ -1,246 +0,0 @@
-// EN: Authorization Controller for OAuth2 Authorization Code Flow
-// VI: Authorization Controller cho OAuth2 Authorization Code Flow
-
-using System.Collections.Immutable;
-using System.Security.Claims;
-using Microsoft.AspNetCore;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Identity;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.IdentityModel.Tokens;
-using OpenIddict.Abstractions;
-using OpenIddict.Server.AspNetCore;
-using Swashbuckle.AspNetCore.Annotations;
-using IamService.Domain.AggregatesModel.UserAggregate;
-using static OpenIddict.Abstractions.OpenIddictConstants;
-
-namespace IamService.API.Controllers;
-
-///
-/// EN: Handles OAuth2 Authorization Code Flow with PKCE.
-/// VI: Xử lý OAuth2 Authorization Code Flow với PKCE.
-///
-[ApiController]
-[SwaggerTag("OAuth2 Authorization endpoints - Authorization Code Flow with PKCE")]
-public class AuthorizationController : ControllerBase
-{
- private readonly IOpenIddictApplicationManager _applicationManager;
- private readonly IOpenIddictAuthorizationManager _authorizationManager;
- private readonly IOpenIddictScopeManager _scopeManager;
- private readonly SignInManager _signInManager;
- private readonly UserManager _userManager;
- private readonly ILogger _logger;
-
- public AuthorizationController(
- IOpenIddictApplicationManager applicationManager,
- IOpenIddictAuthorizationManager authorizationManager,
- IOpenIddictScopeManager scopeManager,
- SignInManager signInManager,
- UserManager userManager,
- ILogger logger)
- {
- _applicationManager = applicationManager;
- _authorizationManager = authorizationManager;
- _scopeManager = scopeManager;
- _signInManager = signInManager;
- _userManager = userManager;
- _logger = logger;
- }
-
- ///
- /// EN: OAuth2 Authorization endpoint - initiates the authorization flow.
- /// VI: OAuth2 Authorization endpoint - khởi tạo authorization flow.
- ///
- /// Challenge or Authorization ticket
- [HttpGet("~/connect/authorize")]
- [HttpPost("~/connect/authorize")]
- [IgnoreAntiforgeryToken]
- [SwaggerOperation(
- Summary = "OAuth2 Authorization Endpoint",
- Description = "Initiates the OAuth2 Authorization Code flow with PKCE. Redirects to login if not authenticated.",
- OperationId = "Authorize")]
- [SwaggerResponse(StatusCodes.Status302Found, "Redirect to login or callback URL")]
- [SwaggerResponse(StatusCodes.Status400BadRequest, "Invalid request parameters")]
- public async Task Authorize()
- {
- var request = HttpContext.GetOpenIddictServerRequest() ??
- throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
-
- // EN: Try to retrieve the user principal stored in the authentication cookie
- // VI: Thử lấy user principal từ authentication cookie
- var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
-
- // EN: If the user is not authenticated, challenge the user
- // VI: Nếu user chưa xác thực, yêu cầu đăng nhập
- if (!result.Succeeded)
- {
- _logger.LogInformation("User not authenticated, redirecting to login");
-
- // EN: Build the authorization request URL to redirect back after login
- // VI: Tạo URL authorization request để redirect sau khi login
- return Challenge(
- authenticationSchemes: IdentityConstants.ApplicationScheme,
- properties: new AuthenticationProperties
- {
- RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
- Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
- });
- }
-
- // EN: Retrieve the user from the authentication result
- // VI: Lấy user từ kết quả authentication
- var user = await _userManager.GetUserAsync(result.Principal);
- if (user == null)
- {
- _logger.LogWarning("User not found from authentication result");
- return Challenge(
- authenticationSchemes: IdentityConstants.ApplicationScheme,
- properties: new AuthenticationProperties
- {
- RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
- Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
- });
- }
-
- // EN: Create the claims-based identity used in the authorization response
- // VI: Tạo claims-based identity cho authorization response
- var identity = new ClaimsIdentity(
- authenticationType: TokenValidationParameters.DefaultAuthenticationType,
- nameType: Claims.Name,
- roleType: Claims.Role);
-
- // EN: Add standard claims
- // VI: Thêm standard claims
- identity.AddClaim(Claims.Subject, user.Id.ToString());
- identity.AddClaim(Claims.Name, user.UserName ?? user.Email ?? "");
- identity.AddClaim(Claims.Email, user.Email ?? "");
- identity.AddClaim(Claims.EmailVerified, (user.EmailConfirmed).ToString().ToLower());
-
- if (!string.IsNullOrEmpty(user.FirstName))
- {
- identity.AddClaim(Claims.GivenName, user.FirstName);
- }
- if (!string.IsNullOrEmpty(user.LastName))
- {
- identity.AddClaim(Claims.FamilyName, user.LastName);
- }
-
- // EN: Add roles as claims
- // VI: Thêm roles như claims
- var roles = await _userManager.GetRolesAsync(user);
- foreach (var role in roles)
- {
- identity.AddClaim(Claims.Role, role);
- }
-
- var principal = new ClaimsPrincipal(identity);
-
- // EN: Set requested scopes as granted
- // VI: Set requested scopes là granted
- var scopes = request.GetScopes();
- principal.SetScopes(scopes);
- principal.SetResources(await _scopeManager.ListResourcesAsync(scopes).ToListAsync());
-
- // EN: Create and store an authorization if needed
- // VI: Tạo và lưu authorization nếu cần
- var application = await _applicationManager.FindByClientIdAsync(request.ClientId!);
- if (application != null)
- {
- var applicationId = await _applicationManager.GetIdAsync(application);
-
- // EN: Create a permanent authorization to avoid requiring consent for every request
- // VI: Tạo permanent authorization để tránh yêu cầu consent cho mọi request
- var authorization = await _authorizationManager.CreateAsync(
- principal: principal,
- subject: user.Id.ToString(),
- client: applicationId!,
- type: AuthorizationTypes.Permanent,
- scopes: scopes);
-
- principal.SetAuthorizationId(await _authorizationManager.GetIdAsync(authorization));
- }
-
- _logger.LogInformation("Authorization granted for user {UserId} with scopes {Scopes}",
- user.Id, string.Join(", ", scopes));
-
- // EN: Returning a SignInResult will ask OpenIddict to issue the appropriate tokens
- // VI: Returning SignInResult sẽ yêu cầu OpenIddict issue tokens phù hợp
- return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
- }
-
- ///
- /// EN: OAuth2 Logout endpoint - ends the user session.
- /// VI: OAuth2 Logout endpoint - kết thúc user session.
- ///
- [HttpGet("~/connect/logout")]
- [HttpPost("~/connect/logout")]
- [SwaggerOperation(
- Summary = "OAuth2 Logout Endpoint",
- Description = "Logs out the user and optionally redirects to post_logout_redirect_uri.",
- OperationId = "Logout")]
- [SwaggerResponse(StatusCodes.Status200OK, "Logged out successfully")]
- [SwaggerResponse(StatusCodes.Status302Found, "Redirect to post logout URI")]
- public async Task Logout()
- {
- var request = HttpContext.GetOpenIddictServerRequest();
-
- // EN: Sign out from Identity
- // VI: Đăng xuất khỏi Identity
- await _signInManager.SignOutAsync();
-
- _logger.LogInformation("User logged out via OAuth2 logout endpoint");
-
- // EN: If post_logout_redirect_uri was provided, redirect there
- // VI: Nếu có post_logout_redirect_uri, redirect đến đó
- if (request?.PostLogoutRedirectUri != null)
- {
- return Redirect(request.PostLogoutRedirectUri);
- }
-
- // EN: Return sign out result
- // VI: Return kết quả sign out
- return SignOut(
- authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
- properties: new AuthenticationProperties
- {
- RedirectUri = "/"
- });
- }
-
- ///
- /// EN: Validates that the current user can be authenticated.
- /// VI: Xác thực rằng user hiện tại có thể được xác thực.
- ///
- [HttpGet("~/connect/authorize/callback")]
- [Authorize]
- [SwaggerOperation(
- Summary = "Authorization Callback",
- Description = "Callback endpoint after successful authentication.",
- OperationId = "AuthorizeCallback")]
- [SwaggerResponse(StatusCodes.Status302Found, "Redirect back to authorization")]
- public IActionResult Callback()
- {
- // EN: Redirect back to the authorization endpoint
- // VI: Redirect về authorization endpoint
- var returnUrl = Request.Query["returnUrl"].FirstOrDefault();
- if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
- {
- return Redirect(returnUrl);
- }
-
- return Redirect("/connect/authorize" + Request.QueryString);
- }
-}
-
-///
-/// EN: Extension methods for ClaimsIdentity
-/// VI: Extension methods cho ClaimsIdentity
-///
-internal static class ClaimsIdentityExtensions
-{
- public static void AddClaim(this ClaimsIdentity identity, string type, string value)
- {
- identity.AddClaim(new Claim(type, value));
- }
-}
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 51af8790..43220a37 100644
--- a/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs
+++ b/services/iam-service-net/src/IamService.API/Controllers/RolesController.cs
@@ -1,8 +1,8 @@
using Asp.Versioning;
using MediatR;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using OpenIddict.Validation.AspNetCore;
using Swashbuckle.AspNetCore.Annotations;
using IamService.API.Application.Common;
using IamService.API.Application.Commands.Roles;
@@ -17,7 +17,7 @@ namespace IamService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/roles")]
-[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[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 36c57dc6..f408af00 100644
--- a/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs
+++ b/services/iam-service-net/src/IamService.API/Controllers/UsersController.cs
@@ -1,8 +1,8 @@
using Asp.Versioning;
using MediatR;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using OpenIddict.Validation.AspNetCore;
using Swashbuckle.AspNetCore.Annotations;
using IamService.API.Application.Common;
using IamService.API.Application.Commands.Users;
@@ -17,7 +17,7 @@ namespace IamService.API.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
-[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
+[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[SwaggerTag("User management endpoints - requires authentication")]
public class UsersController : ControllerBase
{
diff --git a/services/iam-service-net/src/IamService.API/IamService.API.csproj b/services/iam-service-net/src/IamService.API/IamService.API.csproj
index 82bb9de9..4e0512a7 100644
--- a/services/iam-service-net/src/IamService.API/IamService.API.csproj
+++ b/services/iam-service-net/src/IamService.API/IamService.API.csproj
@@ -45,9 +45,8 @@
-
-
-
+
+
diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs
index ab5145b2..77aa76a6 100644
--- a/services/iam-service-net/src/IamService.API/Program.cs
+++ b/services/iam-service-net/src/IamService.API/Program.cs
@@ -16,8 +16,8 @@ builder.Host.UseSerilog((context, services, configuration) => configuration
.Enrich.FromLogContext()
.WriteTo.Console());
-// EN: Add Infrastructure services (Identity, OpenIddict, Repositories)
-// VI: Thêm Infrastructure services (Identity, OpenIddict, Repositories)
+// EN: Add Infrastructure services (Identity, Duende IdentityServer, Repositories)
+// VI: Thêm Infrastructure services (Identity, Duende IdentityServer, Repositories)
builder.Services.AddInfrastructure(builder.Configuration, builder.Environment.EnvironmentName);
// EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors
@@ -263,6 +263,7 @@ app.UseCors();
app.UseRouting();
// EN: Authentication and Authorization / VI: Xác thực và phân quyền
+app.UseIdentityServer();
app.UseAuthentication();
app.UseAuthorization();
diff --git a/services/iam-service-net/src/IamService.API/tempkey.jwk b/services/iam-service-net/src/IamService.API/tempkey.jwk
new file mode 100644
index 00000000..be76467e
--- /dev/null
+++ b/services/iam-service-net/src/IamService.API/tempkey.jwk
@@ -0,0 +1 @@
+{"alg":"RS256","d":"Axq4HZJr2EmWq0nvxDAcq8-1JBcYlp5Zppl408TsOUen6PRJEu53jdeRgIg0fpSPc_eDaaFS7bBpfxfZOQDJufDC3BJYXfjizFN_NniJH6JBYwtr7aobhGddaFxQtZBFYpmxXnyYzqiSswqjigD5ByKoFlASw45Wcu7-Zwvopr0sjHSlFuM0GFWS6CCu46CW7BFu6veLC3aRAcrFnwEuU3_I2kwV40BnUWB6D1CUglfZ8Fqsj3yiVGhnN-aYO4yN8uon2Smbz-z1q_ckNpG0Q4GebJ6b7-bHOM9Mk68oMY69tE0DW-c6UeDX_qFchxBi0xgWePezpOPQUc1kHRxEQw","dp":"yo5sGTxkNeR7YJd_2E_dTq_EzsU-mGmLHzSW3nG8_7zNtbniWwTFG-d4R-qBGpiMe3NWRRk2hhboIwsGsjO82wiufA6u5Chzzbj-vsX-9kkYIaOEiecOePoDMuUR63jkho7y-yQj0zRX3ljSmDUx2WsGp4c-bha33iXNS_qAyyc","dq":"pXJ4HBoexLAldTJOweMUQFkzlGXJFSRy_NWYyNTwLdpHC60IXQdAyvGVt68GtNYLRtDSBCXhmRkSdUJex2pCtGtQ2vDkdRgF1Ip4JbUm3KkbmgwtVq4LhwYxbuWd2vKSTK1EuOifk0Eakmj4ta1fVifGnmAw5HYGAZ1wY9NxCW8","e":"AQAB","key_ops":[],"kid":"9F395D43023373C4404FD48CF0F45A06","kty":"RSA","n":"w7hjkNEwExHYEt2omqEG__halBFn-tAql7vjPtxpdFWA09eeSImKHJkFEsSIIrzmJerp_nUd7N_oPGvx7QonVjE0ibd1Lj0E2sMuKwkipJpq9L9YzWZe8NNBUVMiUzwzcHFHGADPq59Nmp5-_Qwywjso_y10w8-7W1pXb8LZnjGaRKghCY_7--0uFLXyG9fWjfqTEtXhsdcAevf9wSaNzATBzJFXiAQxk_8QPqQRLsqic8HS1OcwTBRx8s7xGdDeiNEW-G5IwoCLRCXWTraR8uUAQjOrmoVzJYxWjyJbF0G8ixz9GrAwtKUF-3sVWVKuS-XarKjSSwCpL1wogx6_LQ","oth":[],"p":"84TJPKJOEduhN-lZEhCMICoEwUiQ98Sx8v2I7gpIKumLk3rf4y4yR6f-1R0sJOG6qC-u4_nMoWc6BTpBFa8ebgmYvFH4fXo51pyxdMg2pTz7LjaDc4cMXXSJBLrygE1RKRy0jDjkt5TxVlUIUlmtceiffhOSwWuLulrZQreJpYc","q":"zcBwTgvRft2D7d6geQafL8OMqiozq_zBcWNiJ0nRny1QFwa372dqld-85B_005HOhdoGo_MRPrjWFXc3A2aQFcDTcNwI8q1tWLCc2Ujztrmi6UJ5x2dmF2TJWbazJdlZDdF6oC_XfEybNp0gGTZW01uJ8nDmoYkGjSeTiqyU4qs","qi":"UGVc3HKHzW7plswnG0aiq6UbAZdYqoWfMlcQhvMML1nHDk9ecsKZL7KuKUkSnzL3sE7vmGb5q7uHAVLmrPtWOvLVkP9CMDtNC6db_aj1ilp_te7ulwDO1JUG6zcM27Se6i75XIj0rjY4ZGB4Nqi1kn0niB3rJCf2nt0i1E3FPas","x5c":[]}
\ No newline at end of file
diff --git a/services/iam-service-net/src/IamService.Infrastructure/Data/OpenIddictClientSeeder.cs b/services/iam-service-net/src/IamService.Infrastructure/Data/OpenIddictClientSeeder.cs
deleted file mode 100644
index 7e354f66..00000000
--- a/services/iam-service-net/src/IamService.Infrastructure/Data/OpenIddictClientSeeder.cs
+++ /dev/null
@@ -1,189 +0,0 @@
-// EN: OpenIddict client seeder for development
-// VI: OpenIddict client seeder cho development
-
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using OpenIddict.Abstractions;
-using static OpenIddict.Abstractions.OpenIddictConstants;
-
-namespace IamService.Infrastructure.Data;
-
-///
-/// EN: Background service to seed OpenIddict OAuth2 clients on startup.
-/// VI: Background service để seed OpenIddict OAuth2 clients khi startup.
-///
-public class OpenIddictClientSeeder : IHostedService
-{
- private readonly IServiceProvider _serviceProvider;
- private readonly ILogger _logger;
-
- public OpenIddictClientSeeder(
- IServiceProvider serviceProvider,
- ILogger logger)
- {
- _serviceProvider = serviceProvider;
- _logger = logger;
- }
-
- public async Task StartAsync(CancellationToken cancellationToken)
- {
- await using var scope = _serviceProvider.CreateAsyncScope();
-
- var context = scope.ServiceProvider.GetRequiredService();
- await context.Database.EnsureCreatedAsync(cancellationToken);
-
- var manager = scope.ServiceProvider.GetRequiredService();
-
- // EN: Seed web-app client (Authorization Code + PKCE)
- // VI: Seed web-app client (Authorization Code + PKCE)
- await SeedWebAppClientAsync(manager, cancellationToken);
-
- // EN: Seed mobile-app client (Authorization Code + PKCE for native apps)
- // VI: Seed mobile-app client (Authorization Code + PKCE cho native apps)
- await SeedMobileAppClientAsync(manager, cancellationToken);
-
- // EN: Seed service client (Client Credentials)
- // VI: Seed service client (Client Credentials)
- await SeedServiceClientAsync(manager, cancellationToken);
-
- _logger.LogInformation("OpenIddict clients seeded successfully");
- }
-
- public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
-
- private async Task SeedWebAppClientAsync(
- IOpenIddictApplicationManager manager,
- CancellationToken cancellationToken)
- {
- const string clientId = "web-app";
-
- if (await manager.FindByClientIdAsync(clientId, cancellationToken) != null)
- {
- _logger.LogDebug("Client {ClientId} already exists, skipping", clientId);
- return;
- }
-
- await manager.CreateAsync(new OpenIddictApplicationDescriptor
- {
- ClientId = clientId,
- ClientSecret = "web-app-secret", // EN: Use env variable in production / VI: Dùng env variable trong production
- DisplayName = "Web Application",
- ConsentType = ConsentTypes.Implicit,
- Permissions =
- {
- Permissions.Endpoints.Authorization,
- Permissions.Endpoints.Logout,
- Permissions.Endpoints.Token,
- Permissions.Endpoints.Revocation,
- Permissions.GrantTypes.AuthorizationCode,
- Permissions.GrantTypes.RefreshToken,
- Permissions.ResponseTypes.Code,
- Permissions.Scopes.Email,
- Permissions.Scopes.Profile,
- Permissions.Scopes.Roles,
- Permissions.Prefixes.Scope + "api",
- Permissions.Prefixes.Scope + "offline_access"
- },
- RedirectUris =
- {
- new Uri("http://localhost:3000/auth/callback"),
- new Uri("http://localhost:5173/auth/callback"),
- new Uri("https://localhost:3000/auth/callback")
- },
- PostLogoutRedirectUris =
- {
- new Uri("http://localhost:3000"),
- new Uri("http://localhost:5173"),
- new Uri("https://localhost:3000")
- },
- Requirements =
- {
- Requirements.Features.ProofKeyForCodeExchange // EN: PKCE required / VI: PKCE bắt buộc
- }
- }, cancellationToken);
-
- _logger.LogInformation("Created OAuth2 client: {ClientId}", clientId);
- }
-
- private async Task SeedMobileAppClientAsync(
- IOpenIddictApplicationManager manager,
- CancellationToken cancellationToken)
- {
- const string clientId = "mobile-app";
-
- if (await manager.FindByClientIdAsync(clientId, cancellationToken) != null)
- {
- _logger.LogDebug("Client {ClientId} already exists, skipping", clientId);
- return;
- }
-
- await manager.CreateAsync(new OpenIddictApplicationDescriptor
- {
- ClientId = clientId,
- // EN: No client secret for native/mobile apps (public client)
- // VI: Không có client secret cho native/mobile apps (public client)
- ClientType = ClientTypes.Public,
- DisplayName = "Mobile Application",
- ConsentType = ConsentTypes.Implicit,
- Permissions =
- {
- Permissions.Endpoints.Authorization,
- Permissions.Endpoints.Logout,
- Permissions.Endpoints.Token,
- Permissions.Endpoints.Revocation,
- Permissions.GrantTypes.AuthorizationCode,
- Permissions.GrantTypes.RefreshToken,
- Permissions.ResponseTypes.Code,
- Permissions.Scopes.Email,
- Permissions.Scopes.Profile,
- Permissions.Scopes.Roles,
- Permissions.Prefixes.Scope + "api",
- Permissions.Prefixes.Scope + "offline_access"
- },
- RedirectUris =
- {
- // EN: Custom scheme for mobile apps
- // VI: Custom scheme cho mobile apps
- new Uri("com.goodgo.app://oauth/callback"),
- new Uri("goodgo://auth/callback")
- },
- Requirements =
- {
- Requirements.Features.ProofKeyForCodeExchange // EN: PKCE required / VI: PKCE bắt buộc
- }
- }, cancellationToken);
-
- _logger.LogInformation("Created OAuth2 client: {ClientId}", clientId);
- }
-
- private async Task SeedServiceClientAsync(
- IOpenIddictApplicationManager manager,
- CancellationToken cancellationToken)
- {
- const string clientId = "service-client";
-
- if (await manager.FindByClientIdAsync(clientId, cancellationToken) != null)
- {
- _logger.LogDebug("Client {ClientId} already exists, skipping", clientId);
- return;
- }
-
- await manager.CreateAsync(new OpenIddictApplicationDescriptor
- {
- ClientId = clientId,
- ClientSecret = "service-client-secret", // EN: Use env variable in production / VI: Dùng env variable trong production
- ClientType = ClientTypes.Confidential,
- DisplayName = "Service to Service Client",
- Permissions =
- {
- Permissions.Endpoints.Token,
- Permissions.Endpoints.Introspection,
- Permissions.GrantTypes.ClientCredentials,
- Permissions.Prefixes.Scope + "api"
- }
- }, cancellationToken);
-
- _logger.LogInformation("Created OAuth2 client: {ClientId}", clientId);
- }
-}
diff --git a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs
index a698fef4..206e4b67 100644
--- a/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs
+++ b/services/iam-service-net/src/IamService.Infrastructure/DependencyInjection.cs
@@ -1,3 +1,6 @@
+// EN: Dependency injection extensions for Infrastructure layer.
+// VI: Dependency injection extensions cho Infrastructure layer.
+
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
@@ -5,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
using IamService.Domain.AggregatesModel.UserAggregate;
using IamService.Domain.AggregatesModel.RoleAggregate;
using IamService.Domain.SeedWork;
-using IamService.Infrastructure.Data;
+using IamService.Infrastructure.IdentityServer;
using IamService.Infrastructure.Repositories;
namespace IamService.Infrastructure;
@@ -56,10 +59,6 @@ public static class DependencyInjection
maxRetryDelay: TimeSpan.FromSeconds(10),
errorCodesToAdd: null);
});
-
- // EN: Use OpenIddict EF Core stores
- // VI: Sử dụng OpenIddict EF Core stores
- options.UseOpenIddict();
});
}
@@ -89,71 +88,59 @@ public static class DependencyInjection
.AddEntityFrameworkStores()
.AddDefaultTokenProviders();
- // EN: Configure OpenIddict
- // VI: Cấu hình OpenIddict
- services.AddOpenIddict()
- // EN: Register the OpenIddict core components
- // VI: Đăng ký OpenIddict core components
- .AddCore(options =>
+ // EN: Configure Duende IdentityServer
+ // VI: Cấu hình Duende IdentityServer
+ services.AddIdentityServer(options =>
+ {
+ // EN: Events for logging
+ // VI: Events để logging
+ options.Events.RaiseErrorEvents = true;
+ options.Events.RaiseInformationEvents = true;
+ options.Events.RaiseFailureEvents = true;
+ options.Events.RaiseSuccessEvents = true;
+
+ // EN: Emit static audience claim
+ // VI: Emit static audience claim
+ options.EmitStaticAudienceClaim = true;
+ })
+ .AddInMemoryIdentityResources(Config.IdentityResources)
+ .AddInMemoryApiScopes(Config.ApiScopes)
+ .AddInMemoryApiResources(Config.ApiResources)
+ .AddInMemoryClients(Config.Clients)
+ .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
+ services.AddAuthentication()
+ .AddJwtBearer(options =>
{
- options.UseEntityFrameworkCore()
- .UseDbContext();
- })
- // EN: Register the OpenIddict server components
- // VI: Đăng ký OpenIddict server components
- .AddServer(options =>
- {
- // EN: Enable Authorization Code Flow endpoints
- // VI: Bật Authorization Code Flow endpoints
- options.SetAuthorizationEndpointUris("/connect/authorize")
- .SetLogoutEndpointUris("/connect/logout")
- .SetTokenEndpointUris("/connect/token")
- .SetUserinfoEndpointUris("/connect/userinfo")
- .SetIntrospectionEndpointUris("/connect/introspect")
- .SetRevocationEndpointUris("/connect/revoke");
-
- // EN: Enable flows - including Authorization Code + PKCE
- // VI: Bật flows - bao gồm Authorization Code + PKCE
- options.AllowAuthorizationCodeFlow()
- .RequireProofKeyForCodeExchange() // EN: PKCE required / VI: PKCE bắt buộc
- .AllowPasswordFlow()
- .AllowRefreshTokenFlow()
- .AllowClientCredentialsFlow();
-
- // EN: Register scopes
- // VI: Đăng ký scopes
- options.RegisterScopes("openid", "profile", "email", "roles", "api", "offline_access");
-
- // EN: Token lifetimes
- // VI: Thời hạn token
- options.SetAccessTokenLifetime(TimeSpan.FromMinutes(15))
- .SetRefreshTokenLifetime(TimeSpan.FromDays(7));
-
- // EN: Development settings
- // VI: Cài đặt development
- options.AddDevelopmentEncryptionCertificate()
- .AddDevelopmentSigningCertificate()
- .DisableAccessTokenEncryption(); // EN: Disable encryption for dev / VI: Tắt mã hóa cho dev
-
- // EN: Accept anonymous clients (for password flow without client_id)
- // VI: Chấp nhận anonymous clients (cho password flow không cần client_id)
- options.AcceptAnonymousClients();
-
- // EN: Configure ASP.NET Core integration
- // VI: Cấu hình tích hợp ASP.NET Core
- options.UseAspNetCore()
- .EnableAuthorizationEndpointPassthrough()
- .EnableLogoutEndpointPassthrough()
- .EnableTokenEndpointPassthrough()
- .EnableUserinfoEndpointPassthrough()
- .DisableTransportSecurityRequirement(); // EN: Allow HTTP for dev / VI: Cho phép HTTP cho dev
- })
- // EN: Register the OpenIddict validation components
- // VI: Đăng ký OpenIddict validation components
- .AddValidation(options =>
- {
- options.UseLocalServer();
- options.UseAspNetCore();
+ // 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;
+ options.TokenValidationParameters.SignatureValidator = (token, parameters) => new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(token);
+ }
});
// EN: Register repositories
@@ -171,21 +158,13 @@ public static class DependencyInjection
services.AddSingleton(sp =>
{
- var connectionString = redisSettings.GetConnectionString();
- return StackExchange.Redis.ConnectionMultiplexer.Connect(connectionString);
+ var connString = redisSettings.GetConnectionString();
+ return StackExchange.Redis.ConnectionMultiplexer.Connect(connString);
});
services.AddSingleton();
}
- // EN: Register OpenIddict client seeder (skip in Testing environment)
- // VI: Đăng ký OpenIddict client seeder (bỏ qua trong Testing environment)
- if (!string.Equals(environmentName, "Testing", StringComparison.OrdinalIgnoreCase))
- {
- services.AddHostedService();
- }
-
return services;
}
}
-
diff --git a/services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj b/services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj
index de68ed06..b8af21ed 100644
--- a/services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj
+++ b/services/iam-service-net/src/IamService.Infrastructure/IamService.Infrastructure.csproj
@@ -31,9 +31,13 @@
-
-
-
+
+
+
+
+
+
+
diff --git a/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs b/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs
index 0a299a83..f0a544d0 100644
--- a/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs
+++ b/services/iam-service-net/src/IamService.Infrastructure/IamServiceContext.cs
@@ -10,8 +10,8 @@ using IamService.Domain.SeedWork;
namespace IamService.Infrastructure;
///
-/// EN: Database context for IAM Service with Identity and OpenIddict support.
-/// VI: Database context cho IAM Service với Identity và OpenIddict support.
+/// EN: Database context for IAM Service with Identity support.
+/// VI: Database context cho IAM Service với Identity support.
///
public class IamServiceContext : IdentityDbContext, IUnitOfWork
{
@@ -69,10 +69,6 @@ public class IamServiceContext : IdentityDbContext
diff --git a/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs
new file mode 100644
index 00000000..a65894f9
--- /dev/null
+++ b/services/iam-service-net/src/IamService.Infrastructure/IdentityServer/Config.cs
@@ -0,0 +1,171 @@
+// EN: IdentityServer Configuration - Clients, Scopes, Resources
+// VI: IdentityServer Configuration - Clients, Scopes, Resources
+
+using Duende.IdentityServer;
+using Duende.IdentityServer.Models;
+
+namespace IamService.Infrastructure.IdentityServer;
+
+///
+/// EN: Static configuration for IdentityServer.
+/// VI: Static configuration cho IdentityServer.
+///
+public static class Config
+{
+ ///
+ /// EN: Identity resources (claims about the user).
+ /// VI: Identity resources (claims về user).
+ ///
+ public static IEnumerable IdentityResources =>
+ [
+ new IdentityResources.OpenId(),
+ new IdentityResources.Profile(),
+ new IdentityResources.Email(),
+ new IdentityResource("roles", "User roles", ["role"])
+ ];
+
+ ///
+ /// EN: API scopes for protecting APIs.
+ /// VI: API scopes để bảo vệ APIs.
+ ///
+ public static IEnumerable ApiScopes =>
+ [
+ new ApiScope("api", "IAM Service API")
+ {
+ UserClaims = ["role", "email", "name"]
+ }
+ ];
+
+ ///
+ /// EN: API resources (logical grouping of scopes).
+ /// VI: API resources (nhóm logic của scopes).
+ ///
+ public static IEnumerable ApiResources =>
+ [
+ new ApiResource("iam-api", "IAM Service API")
+ {
+ Scopes = { "api" },
+ UserClaims = { "role", "email", "name" }
+ }
+ ];
+
+ ///
+ /// EN: OAuth2/OIDC Clients.
+ /// VI: OAuth2/OIDC Clients.
+ ///
+ public static IEnumerable Clients =>
+ [
+ // EN: Web Application - Authorization Code + PKCE
+ // VI: Web Application - Authorization Code + PKCE
+ new Client
+ {
+ ClientId = "web-app",
+ ClientName = "Web Application",
+ ClientSecrets = { new Secret("web-app-secret".Sha256()) },
+
+ AllowedGrantTypes = GrantTypes.Code,
+ RequirePkce = true,
+ RequireClientSecret = true,
+
+ RedirectUris =
+ {
+ "http://localhost:3000/auth/callback",
+ "http://localhost:5173/auth/callback",
+ "https://localhost:3000/auth/callback"
+ },
+ PostLogoutRedirectUris =
+ {
+ "http://localhost:3000",
+ "http://localhost:5173",
+ "https://localhost:3000"
+ },
+
+ AllowedScopes =
+ {
+ IdentityServerConstants.StandardScopes.OpenId,
+ IdentityServerConstants.StandardScopes.Profile,
+ IdentityServerConstants.StandardScopes.Email,
+ IdentityServerConstants.StandardScopes.OfflineAccess,
+ "roles",
+ "api"
+ },
+
+ AllowOfflineAccess = true,
+ AccessTokenLifetime = 900, // 15 minutes
+ RefreshTokenExpiration = TokenExpiration.Sliding,
+ SlidingRefreshTokenLifetime = 604800 // 7 days
+ },
+
+ // EN: Mobile Application - Authorization Code + PKCE (Public Client)
+ // VI: Mobile Application - Authorization Code + PKCE (Public Client)
+ new Client
+ {
+ ClientId = "mobile-app",
+ ClientName = "Mobile Application",
+
+ AllowedGrantTypes = GrantTypes.Code,
+ RequirePkce = true,
+ RequireClientSecret = false, // EN: Public client / VI: Public client
+
+ RedirectUris =
+ {
+ "com.goodgo.app://oauth/callback",
+ "goodgo://auth/callback"
+ },
+
+ AllowedScopes =
+ {
+ IdentityServerConstants.StandardScopes.OpenId,
+ IdentityServerConstants.StandardScopes.Profile,
+ IdentityServerConstants.StandardScopes.Email,
+ IdentityServerConstants.StandardScopes.OfflineAccess,
+ "roles",
+ "api"
+ },
+
+ AllowOfflineAccess = true,
+ AccessTokenLifetime = 900,
+ RefreshTokenExpiration = TokenExpiration.Sliding,
+ SlidingRefreshTokenLifetime = 604800
+ },
+
+ // EN: Service-to-Service - Client Credentials
+ // VI: Service-to-Service - Client Credentials
+ new Client
+ {
+ ClientId = "service-client",
+ ClientName = "Service to Service Client",
+ ClientSecrets = { new Secret("service-client-secret".Sha256()) },
+
+ AllowedGrantTypes = GrantTypes.ClientCredentials,
+
+ AllowedScopes = { "api" }
+ },
+
+ // EN: Resource Owner Password (for legacy/testing)
+ // VI: Resource Owner Password (cho legacy/testing)
+ new Client
+ {
+ ClientId = "password-client",
+ ClientName = "Password Grant Client",
+ ClientSecrets = { new Secret("password-client-secret".Sha256()) },
+
+ AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
+
+ AllowedScopes =
+ {
+ IdentityServerConstants.StandardScopes.OpenId,
+ IdentityServerConstants.StandardScopes.Profile,
+ IdentityServerConstants.StandardScopes.Email,
+ IdentityServerConstants.StandardScopes.OfflineAccess,
+ "roles",
+ "api"
+ },
+
+ AllowOfflineAccess = true,
+ AccessTokenLifetime = 900,
+ RefreshTokenExpiration = TokenExpiration.Sliding,
+ SlidingRefreshTokenLifetime = 604800
+ }
+ ];
+}
diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs
index b8ae2289..82a6d184 100644
--- a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs
+++ b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/AuthControllerTests.cs
@@ -152,13 +152,15 @@ public class AuthControllerTests : IClassFixture
await userManager.UpdateAsync(user);
}
- // Act - Login
+ // Act - Login using Duende IdentityServer format
var tokenRequest = new FormUrlEncodedContent(new Dictionary
{
["grant_type"] = "password",
+ ["client_id"] = "password-client",
+ ["client_secret"] = "password-client-secret",
["username"] = email,
["password"] = password,
- ["scope"] = "openid profile email offline_access"
+ ["scope"] = "openid profile email api offline_access"
});
var response = await _client.PostAsync("/connect/token", tokenRequest);
@@ -182,13 +184,15 @@ public class AuthControllerTests : IClassFixture
[Fact]
public async Task Token_WithInvalidCredentials_ShouldReturn400()
{
- // Arrange
+ // Arrange - using Duende IdentityServer format
var tokenRequest = new FormUrlEncodedContent(new Dictionary
{
["grant_type"] = "password",
+ ["client_id"] = "password-client",
+ ["client_secret"] = "password-client-secret",
["username"] = "nonexistent@example.com",
["password"] = "WrongPassword123!",
- ["scope"] = "openid"
+ ["scope"] = "openid api"
});
// Act
diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/UsersControllerTests.cs b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/UsersControllerTests.cs
index da038fe5..e7e4665b 100644
--- a/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/UsersControllerTests.cs
+++ b/services/iam-service-net/tests/IamService.FunctionalTests/Controllers/UsersControllerTests.cs
@@ -52,13 +52,15 @@ public class UsersControllerTests : IClassFixture
await userManager.UpdateAsync(user);
}
- // Get token
+ // Get token using Duende IdentityServer format (includes client credentials)
var tokenRequest = new FormUrlEncodedContent(new Dictionary
{
["grant_type"] = "password",
+ ["client_id"] = "password-client",
+ ["client_secret"] = "password-client-secret",
["username"] = email,
["password"] = password,
- ["scope"] = "openid profile email offline_access"
+ ["scope"] = "openid profile email api offline_access"
});
var response = await _client.PostAsync("/connect/token", tokenRequest);
diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs
index 298319a7..aece5431 100644
--- a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs
+++ b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs
@@ -6,13 +6,16 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using IamService.Infrastructure;
using IamService.Infrastructure.Caching;
+using IamService.Domain.AggregatesModel.UserAggregate;
+using IamService.Domain.AggregatesModel.RoleAggregate;
+using Microsoft.AspNetCore.Identity;
using StackExchange.Redis;
namespace IamService.FunctionalTests;
///
-/// EN: Custom WebApplicationFactory for functional tests.
-/// VI: WebApplicationFactory tùy chỉnh cho functional tests.
+/// EN: Custom WebApplicationFactory for functional tests with Duende IdentityServer.
+/// VI: WebApplicationFactory tùy chỉnh cho functional tests với Duende IdentityServer.
///
public class CustomWebApplicationFactory : WebApplicationFactory
{
@@ -34,10 +37,6 @@ public class CustomWebApplicationFactory : WebApplicationFactory
// VI: Xóa các đăng ký liên quan đến Redis
RemoveRedisRegistrations(services);
- // EN: Remove OpenIddict EF Core stores to re-register with in-memory db
- // VI: Xóa OpenIddict EF Core stores để đăng ký lại với in-memory db
- RemoveOpenIddictStores(services);
-
// EN: Add mock cache service for testing
// VI: Thêm mock cache service để test
services.AddSingleton();
@@ -47,26 +46,16 @@ public class CustomWebApplicationFactory : WebApplicationFactory
services.AddDbContext(options =>
{
options.UseInMemoryDatabase(_databaseName);
- options.UseOpenIddict();
options.EnableSensitiveDataLogging();
});
- // EN: Re-register OpenIddict Core with the new DbContext
- // VI: Đăng ký lại OpenIddict Core với DbContext mới
- services.AddOpenIddict()
- .AddCore(options =>
- {
- options.UseEntityFrameworkCore()
- .UseDbContext();
- });
-
// EN: Set logging level for debugging tests
// VI: Đặt mức logging để debug tests
services.AddLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Warning);
logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
- logging.AddFilter("OpenIddict", LogLevel.Warning);
+ logging.AddFilter("Duende.IdentityServer", LogLevel.Warning);
});
});
}
@@ -109,21 +98,6 @@ public class CustomWebApplicationFactory : WebApplicationFactory
}
}
- private static void RemoveOpenIddictStores(IServiceCollection services)
- {
- // EN: Remove OpenIddict EF Core store registrations to re-register with in-memory db
- // VI: Xóa các đăng ký OpenIddict EF Core stores để đăng ký lại với in-memory db
- var openIddictDescriptors = services.Where(d =>
- d.ServiceType.FullName?.Contains("OpenIddict.EntityFrameworkCore") == true ||
- d.ImplementationType?.FullName?.Contains("OpenIddict.EntityFrameworkCore") == true)
- .ToList();
-
- foreach (var descriptor in openIddictDescriptors)
- {
- services.Remove(descriptor);
- }
- }
-
///
/// EN: Ensure database is created after host is built
/// VI: Đảm bảo database được tạo sau khi host được build
diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/IamService.FunctionalTests.csproj b/services/iam-service-net/tests/IamService.FunctionalTests/IamService.FunctionalTests.csproj
index e4a209e6..c0aa30c3 100644
--- a/services/iam-service-net/tests/IamService.FunctionalTests/IamService.FunctionalTests.csproj
+++ b/services/iam-service-net/tests/IamService.FunctionalTests/IamService.FunctionalTests.csproj
@@ -21,6 +21,9 @@
+
+
+