using System.Net.Http;
using System.Text;
using System.Text.Json;
namespace AxCopilot.Services;
///
/// IBM Cloud IAM 액세스 토큰 발급 및 캐싱 서비스.
/// API 키를 IAM 토큰으로 교환한 뒤 Bearer 토큰으로 재사용합니다.
///
internal sealed class IbmIamTokenService
{
private static readonly HttpClient _http = new()
{
Timeout = TimeSpan.FromSeconds(15)
};
private static readonly Dictionary _cache = new();
private static readonly object _lock = new();
private const string DefaultIamUrl = "https://iam.cloud.ibm.com/identity/token";
public static async Task GetTokenAsync(string apiKey, string? iamUrl = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(apiKey))
return null;
var tokenUrl = string.IsNullOrWhiteSpace(iamUrl) ? DefaultIamUrl : iamUrl.Trim();
var cacheKey = $"{tokenUrl}|{apiKey}";
lock (_lock)
{
if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow.AddMinutes(1))
return cached.Token;
}
try
{
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl);
req.Content = new StringContent(
$"grant_type=urn:ibm:params:oauth:grant-type:apikey&apikey={Uri.EscapeDataString(apiKey)}",
Encoding.UTF8,
"application/x-www-form-urlencoded");
req.Headers.Accept.ParseAdd("application/json");
using var resp = await _http.SendAsync(req, ct);
if (!resp.IsSuccessStatusCode)
{
var errBody = await resp.Content.ReadAsStringAsync(ct);
LogService.Warn($"IBM IAM 토큰 발급 실패: {resp.StatusCode} - {errBody}");
return null;
}
var json = await resp.Content.ReadAsStringAsync(ct);
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("access_token", out var tokenProp))
{
LogService.Warn("IBM IAM 응답에 access_token 필드가 없습니다.");
return null;
}
var token = tokenProp.GetString();
if (string.IsNullOrWhiteSpace(token))
return null;
var expiry = DateTime.UtcNow.AddMinutes(55);
if (doc.RootElement.TryGetProperty("expiration", out var expirationProp) &&
expirationProp.TryGetInt64(out var expirationEpoch))
{
expiry = DateTimeOffset.FromUnixTimeSeconds(expirationEpoch).UtcDateTime;
}
else if (doc.RootElement.TryGetProperty("expires_in", out var expiresInProp) &&
expiresInProp.TryGetInt64(out var expiresInSeconds))
{
expiry = DateTime.UtcNow.AddSeconds(expiresInSeconds);
}
lock (_lock)
{
_cache[cacheKey] = (token, expiry);
}
LogService.Info($"IBM IAM 토큰 발급 완료: {tokenUrl} (만료: {expiry:yyyy-MM-dd HH:mm} UTC)");
return token;
}
catch (Exception ex)
{
LogService.Error($"IBM IAM 토큰 발급 오류: {ex.Message}");
return null;
}
}
public static void InvalidateToken(string apiKey, string? iamUrl = null)
{
if (string.IsNullOrWhiteSpace(apiKey))
return;
var tokenUrl = string.IsNullOrWhiteSpace(iamUrl) ? DefaultIamUrl : iamUrl.Trim();
var cacheKey = $"{tokenUrl}|{apiKey}";
lock (_lock) { _cache.Remove(cacheKey); }
}
public static void ClearAllTokens()
{
lock (_lock) { _cache.Clear(); }
}
}