using EntitlementBLL.Auth; using EntitlementSL.Common; using FastEndpoints; using GB5Shared.DTO.Framework.Login; using GB5Shared.DTO.Framework.ResponseStandard; using GB5Shared.FastEndPoint; using Microsoft.AspNetCore.RateLimiting; using ErrorResponse = GB5Shared.Resource.Response.ErrorResponse; using static GB5Shared.GB5Constant.Constant; namespace EntitlementSL.Endpoints.ClientAuth; // AllowAnonymous — the refresh token itself is the credential; there is no separate caller // identity to check first. // // Both rejection reasons (Invalid and ReuseDetected) are deliberately mapped to the exact same // generic response here — never distinguishing them to the caller, so an attacker who triggers // reuse-detection gets no signal that they specifically did so (vs. simply presenting an unknown // or expired token). The distinct condition IS preserved server-side — ClientAuthBLL.RefreshAsync // logs it explicitly and cascade-revokes the chain on ReuseDetected — for a future audit/alerting // item to consume. public class RefreshToken : BaseEndpoint> { private readonly IClientAuthBLL _ClientAuthBLL; public RefreshToken(IClientAuthBLL clientAuthBLL) => _ClientAuthBLL = clientAuthBLL; public override void Configure() { Post("/lic/ClientAuth.svc/RefreshToken"); AllowAnonymous(); // Shares the Login/RefreshToken policy bucket — see ClientAuthRateLimitOptions/Program.cs. Options(x => x.RequireRateLimiting(ClientAuthRateLimitPolicies.LoginAndRefresh)); } public record Body(string RawRefreshToken); public record Params( [property: FromHeader] string Login, [property: FromBody] Body RequestBody ); protected override string? GetCacheKey(Params req, LoginDTO loginDTO) => null; protected override async Task> ExecuteAsync(Params req, LoginDTO loginDTO, CancellationToken ct) { var result = await _ClientAuthBLL.RefreshAsync(req.RequestBody.RawRefreshToken, loginDTO, ct); if (result.Status != ClientRefreshResultStatus.Success || result.TokenPair is null) return await GB5Shared.ResponseStandard.Response.CreateErrorResponse( ErrorResponse.InvalidOrExpiredSessionMessage, CacheKeyLevel.NOT_REQUIRED, loginDTO, statusCode: 401); return await GB5Shared.ResponseStandard.Response.CreateSuccessResponse( ClientTokenResponse.From(result.TokenPair), CacheKeyLevel.NOT_REQUIRED, loginDTO); } }