using CORE.APP.Models.Authentication;
using CORE.APP.Services;
using CORE.APP.Services.Authentication;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Users.APP.Domain;
namespace Users.APP.Features.Tokens
{
/// <summary>
/// Represents the request for returning a token response including JWT (access token) and refresh token.
/// Inherits from the RefreshTokenRequestBase class to have token, refresh token and security key in the request
/// and implements IRequest<TokenResponse> for MediatR pipeline integration.
/// </summary>
public class RefreshTokenRequest : RefreshTokenRequestBase, IRequest<TokenResponse>
{
}
/// <summary>
/// Handles the logic for processing a refresh token request to return a token response.
/// Validates if the refresh token is expired or not through a User entity query and returns a new token response
/// including the new JWT (access token) and new refresh token if valid, otherwise returns null.
/// </summary>
public class RefreshTokenHandler : Service<User>, IRequestHandler<RefreshTokenRequest, TokenResponse>
{
/// <summary>
/// The token authentication service that will provide token operations in the methods of this class.
/// </summary>
private readonly ITokenAuthService _tokenAuthService; // readonly means the value or instance can only be assigned
// at this line or through the constructor
/// <summary>
/// Initializes a new instance of this class with the given database context and token authentication service.
/// </summary>
/// <param name="db">The UsersDb database context.</param>
/// <param name="tokenAuthService">The injected token authentication service instance through the IoC Container.</param>
public RefreshTokenHandler(DbContext db, ITokenAuthService tokenAuthService) : base(db)
{
_tokenAuthService = tokenAuthService;
}
/// <summary>
/// Overridden method of the base class for eager loading of UserRole and Role entities.
/// </summary>
/// <param name="isNoTracking">If true, changes of the entities in the DbSet will not be tracked by Entity Framework,
/// otherwise changes will be tracked.</param>
/// <returns>The eagerly loaded User entity query with UserRoles and Role.</returns>
protected override IQueryable<User> Query(bool isNoTracking = true)
{
// u: User entity delegate, ur: UserRole entity delegate
return base.Query(isNoTracking).Include(u => u.UserRoles).ThenInclude(ur => ur.Role);
}
/// <summary>
/// Handles the refresh token logic: verifies the user with refresh token expiration, then returns a new token response
/// including the JWT (access token) and refresh token if verified. Otherwise returns null.
/// </summary>
/// <param name="request">The refresh token request object containing the JWT (access token), refresh token and security key.</param>
/// <param name="cancellationToken">Asynchronous method's token to cancel the operation.</param>
/// <returns>A token response containing the result of the operation.</returns>
public async Task<TokenResponse> Handle(RefreshTokenRequest request, CancellationToken cancellationToken)
{
// Extract the user's claims from request's expired token and security key
var claims = _tokenAuthService.GetClaims(request.Token, request.SecurityKey);
// Extract the user ID from claims
var userId = Convert.ToInt32(claims.SingleOrDefault(claim => claim.Type == "Id").Value);
// Find user in the Users Table that matches the ID and has a non expired refresh token
// isNoTracking is false for being tracked by EF Core to update the entity
var user = await Query(false).SingleOrDefaultAsync(user => user.Id == userId && user.RefreshToken == request.RefreshToken
&& user.RefreshTokenExpiration >= DateTime.Now, cancellationToken);
// If user is not found, return null
if (user is null)
return null;
// Generate a new refresh token (for added security)
user.RefreshToken = _tokenAuthService.GetRefreshToken();
// Optional: Enable sliding expiration for the refresh token
// user.RefreshTokenExpiration = DateTime.Now.AddDays(7);
// Save the updated user state to the database
await Update(user, cancellationToken);
// return a token response according to the expiration including the JWT and refresh token.
var expiration = DateTime.Now.AddMinutes(5);
return _tokenAuthService.GetTokenResponse(user.Id, user.UserName, user.UserRoles.Select(ur => ur.Role.Name).ToArray(),
expiration, request.SecurityKey, request.Issuer, request.Audience, user.RefreshToken);
}
}
}