using System.Security.Cryptography; using System.Text; using GB5Shared.Auth.Jwt; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PartnerBLL.Options; using PartnerDAL.CustomCode.PartnerApiKey; namespace PartnerBLL.Auth; public class PartnerTokenBLL : IPartnerTokenBLL { // Vault path shared with Entitlement's "PartnerM2MJwtBearer" validation scheme — see // PartnerM2MJwtOptions' doc comment for why this one signing key is deliberately shared // across the two modules rather than each having its own. private const string SigningKeyVaultPath = "partner/m2m-jwt-signing-key"; private readonly IPartnerApiKeyDAL _ApiKeyDAL; private readonly IJwtAccessTokenIssuer _Issuer; private readonly PartnerM2MJwtOptions _Options; private readonly ILogger _Logger; public PartnerTokenBLL( IPartnerApiKeyDAL apiKeyDAL, IJwtAccessTokenIssuer issuer, IOptions options, ILogger logger) { _ApiKeyDAL = apiKeyDAL; _Issuer = issuer; _Options = options.Value; _Logger = logger; } public async Task IssueTokenAsync(string rawApiKey, CancellationToken ct) { if (string.IsNullOrWhiteSpace(rawApiKey)) return new PartnerTokenResult { Status = PartnerTokenResultStatus.InvalidApiKey }; try { // Same SHA-256-hex-lowercase hash ApiKeyAuthMiddleware already computes — must match // exactly, since both look up the same TPARTNERAPIKEY.HASHEDKEY column. var hashedKey = HashApiKey(rawApiKey.Trim()); var keyRecord = await _ApiKeyDAL.GetApiKeyByHashFromSystemDbAsync(hashedKey, ct).ConfigureAwait(false); if (keyRecord is null || keyRecord.PartnerId is null) { _Logger.LogWarning( "PartnerTokenBLL: invalid or expired API key presented (hint: last-4={Hint})", rawApiKey.Length >= 4 ? rawApiKey[^4..] : "???"); return new PartnerTokenResult { Status = PartnerTokenResultStatus.InvalidApiKey }; } var claims = new PartnerM2MAccessTokenClaims { PartnerId = keyRecord.PartnerId.Value, PartnerProductId = keyRecord.PartnerProductId, ApiKeyId = keyRecord.ApiKeyId, Scopes = keyRecord.Scopes }; var issued = await _Issuer.IssueAsync( claims, SigningKeyVaultPath, _Options.Issuer, _Options.Audience, _Options.AccessTokenMinutes, ct) .ConfigureAwait(false); // Fire-and-forget, mirrors ApiKeyAuthMiddleware's own identical LastUsedUtc step. _ = Task.Run(async () => { try { await _ApiKeyDAL.UpdateApiKeyLastUsedInSystemDbAsync(keyRecord.ApiKeyId, CancellationToken.None) .ConfigureAwait(false); } catch (Exception ex) { _Logger.LogWarning(ex, "PartnerTokenBLL: LastUsedUtc update failed for ApiKeyId {ApiKeyId}", keyRecord.ApiKeyId); } }, CancellationToken.None); _Logger.LogInformation( "PartnerTokenBLL: issued M2M token | ApiKeyId={ApiKeyId} PartnerProductId={PartnerProductId}", keyRecord.ApiKeyId, keyRecord.PartnerProductId); return new PartnerTokenResult { Status = PartnerTokenResultStatus.Success, AccessToken = issued.AccessToken, ExpiresOn = issued.ExpiresOn }; } catch (Exception ex) { _Logger.LogError(ex, "PartnerTokenBLL: IssueTokenAsync failed unexpectedly."); return new PartnerTokenResult { Status = PartnerTokenResultStatus.InvalidApiKey }; } } private static string HashApiKey(string rawKey) { var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(rawKey)); return Convert.ToHexString(bytes).ToLowerInvariant(); } }