using System.Text; using System.Text.Json; namespace Kbx.Shared.Providers; public interface IKisAccessTokenProvider { ValueTask GetAsync(bool sandbox, CancellationToken cancellationToken); } public sealed class KisAccessTokenProvider(HttpClient http, IKbxProviderSecretStore secrets) : IKisAccessTokenProvider { private readonly SemaphoreSlim _gate = new(1,1); private string? _token; private DateTimeOffset _expiresAt; private bool _sandbox; public async ValueTask GetAsync(bool sandbox, CancellationToken ct) { if (_token is not null && _sandbox == sandbox && DateTimeOffset.UtcNow < _expiresAt.AddMinutes(-5)) return _token; await _gate.WaitAsync(ct); try { if (_token is not null && _sandbox == sandbox && DateTimeOffset.UtcNow < _expiresAt.AddMinutes(-5)) return _token; var appKey = await secrets.GetRequiredAsync("ExternalProviders:Kis:AppKey", ct); var appSecret = await secrets.GetRequiredAsync("ExternalProviders:Kis:AppSecret", ct); var baseUri = sandbox ? "https://openapivts.koreainvestment.com:29443" : "https://openapi.koreainvestment.com:9443"; var payload = JsonSerializer.Serialize(new { grant_type="client_credentials", appkey=appKey, appsecret=appSecret }); using var req = new HttpRequestMessage(HttpMethod.Post, baseUri + "/oauth2/tokenP") { Content = new StringContent(payload, Encoding.UTF8, "application/json") }; using var res = await http.SendAsync(req, ct); res.EnsureSuccessStatusCode(); using var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync(ct)); _token = doc.RootElement.GetProperty("access_token").GetString() ?? throw new InvalidOperationException("KIS access_token missing"); _sandbox = sandbox; if (doc.RootElement.TryGetProperty("access_token_token_expired", out var exp) && DateTimeOffset.TryParse(exp.GetString(), out var parsed)) _expiresAt = parsed; else if (doc.RootElement.TryGetProperty("expires_in", out var sec) && sec.TryGetInt32(out var seconds)) _expiresAt = DateTimeOffset.UtcNow.AddSeconds(seconds); else _expiresAt = DateTimeOffset.UtcNow.AddHours(24); return _token; } finally { _gate.Release(); } } }