using EntitlementBLL.Options;
using GB5Shared.Auth.Jwt;
using Microsoft.Extensions.Options;
namespace EntitlementBLL.Auth;
// Claim names are the ones a future ClientBaseEndpoint/ClientCallerContext (mirroring
// DXPSL/Common/DXPBaseEndpoint.cs) will read when reconstructing caller identity server-side —
// never accept these values from the request body/headers, only from HttpContext.User.Claims
// after JWT middleware has verified the signature.
public static class ClientJwtClaimTypes
{
public const string ClientUserId = "client_user_id";
public const string ClientId = "client_id";
public const string Role = "role";
/// Comma-joined list of EntitlementClientCapabilityCodes values (e.g.
/// "TECH_ADMIN,COMMERCIAL_ADMIN") — empty string when the caller holds none. Mirrors how
/// Partner's M2M claims carry Scopes.
public const string Capabilities = "capabilities";
}
// The literal strings carried in the Role claim (ClientJwtClaimTypes.Role) — also the literal
// FastEndpoints Roles() checks against, mirroring the JWT-claim-only role convention already
// established elsewhere in this codebase (see the gb-ent-admin menu-seed migration notes on
// GOODBOOKS_ADMIN: roles are DB-numeric everywhere except the JWT claim itself). Maps 1:1 to
// EntitlementDAL.Enums.ClientUserRoleEnum (MENTITLEMENTCLIENTUSER.ROLE).
public static class ClientRoleCodes
{
public const string ClientAdmin = "CLIENT_ADMIN";
public const string ClientUser = "CLIENT_USER";
}
// Thin per-module wrapper over the shared GB5Shared.Auth.Jwt issuance mechanics — mirrors
// DXPBLL.Auth.DXPJwtService's identical shape. The actual HMAC-SHA256 signing and Vault-backed
// key resolution now live in GB5Shared (IJwtAccessTokenIssuer/IJwtSigningKeyResolver), shared
// with DXP's DXPJwtService instead of each hand-rolling its own copy.
public class ClientJwtService : IClientJwtService
{
// Vault path unchanged from before this pass — only how it's resolved changed (via the
// shared IJwtSigningKeyResolver -> GB5Shared.Vault.IVaultService, not a module-local resolver).
private const string SigningKeyVaultPath = "entitlement/jwt-signing-key";
private readonly IJwtAccessTokenIssuer _Issuer;
private readonly ClientJwtOptions _Options;
public ClientJwtService(IJwtAccessTokenIssuer issuer, IOptions options)
{
_Issuer = issuer;
_Options = options.Value;
}
public async Task IssueTokenPairAsync(ClientAccessTokenClaims claims, CancellationToken ct)
{
// Single source of truth for both lifetimes, computed once inside the shared issuer from
// ClientJwtOptions.AccessTokenMinutes — then reused verbatim for the JWT's own `expires`
// argument AND the ClientTokenPair values reported back to the caller. This is the
// structural fix for the drift bug found in DXP's AuthBLL.IssueTokensAsync, where a
// second call site independently hardcoded "15"/"30" instead of reusing what the JWT
// service itself computed.
var issued = await _Issuer.IssueAsync(
claims, SigningKeyVaultPath, _Options.Issuer, _Options.Audience, _Options.AccessTokenMinutes, ct)
.ConfigureAwait(false);
var (rawRefreshToken, refreshTokenHash) = RefreshTokenHelper.GenerateRefreshToken();
return new ClientTokenPair
{
AccessToken = issued.AccessToken,
AccessTokenExpiresOn = issued.ExpiresOn,
RefreshToken = rawRefreshToken,
RefreshTokenHash = refreshTokenHash,
RefreshTokenExpiresOn = DateTime.UtcNow.AddDays(_Options.RefreshTokenDays)
};
}
public (string RawToken, string TokenHash) GenerateRefreshToken()
=> RefreshTokenHelper.GenerateRefreshToken();
public string HashRefreshToken(string rawToken)
=> RefreshTokenHelper.HashRefreshToken(rawToken);
}