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); }