using System.Net.Http.Headers; using System.Net.Http.Json; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using OpenHarbor.JwtTokenManager.Abstractions; namespace OpenHarbor.JwtTokenManager; public class JwtTokenManagerService(JwtTokenManagerOptions options, IHttpClientFactory httpClientFactory, ILogger? logger, IMemoryCache? memoryCache, JwtTokenManagerCacheOptions cacheOptions) : IJwtTokenManagerService { private readonly HttpClient _httpClient = httpClientFactory.CreateClient(); private readonly TimeSpan _cacheExpirationOffset = TimeSpan.FromSeconds(cacheOptions.ExpirationOffset); private readonly string _scopes = string.Join(" ", options.Scopes); public async Task GetTokenAsync(CancellationToken cancellationToken = default) { if (memoryCache != null) { var memoryGetValueResult = memoryCache.TryGetValue(cacheOptions.CacheKey, out JwtTokenManagerResult? cachedToken); if (memoryGetValueResult && null != cachedToken) { return cachedToken; } } var formContent = new FormUrlEncodedContent([ new KeyValuePair("grant_type", "client_credentials"), new KeyValuePair("client_id", options.ClientId), new KeyValuePair("client_secret", options.ClientSecret), new KeyValuePair("scopes", string.Join(" ", _scopes)) ]); var request = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint) { Content = formContent }; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await _httpClient.SendAsync(request, cancellationToken); response.EnsureSuccessStatusCode(); var tokenResponse = await response.Content.ReadFromJsonAsync(JwtTokenManagerJsonContext.Default.JwtTokenResponse, cancellationToken); if (tokenResponse == null) { throw new InvalidOperationException("Failed to deserialize the token response content."); } var parsedResult = Enum.TryParse(tokenResponse.TokenType, out var tokenType); if (parsedResult == false) { throw new InvalidOperationException($"Unsupported token type: {tokenResponse.TokenType}"); } var now = DateTime.UtcNow; var expiration = TimeSpan.FromSeconds(tokenResponse.ExpiresIn); var expiresAt = now.Add(expiration); var result = new JwtTokenManagerResult { AccessToken = tokenResponse.AccessToken, TokenType = tokenType, ExpiresAt = expiresAt, ExpiresIn = tokenResponse.ExpiresIn, }; if (null != memoryCache && expiration < _cacheExpirationOffset) { logger?.LogWarning("Caching is enable but the token expiration time [{expiration}] is less than the expiration offset [{cacheExpirationOffset}]. Caching is ignored, please validate your authorization server configuration and the {className} cache expiration offset configuration.", expiration, _cacheExpirationOffset, nameof(JwtTokenManagerService)); } else memoryCache?.Set(cacheOptions.CacheKey, result, expiration.Subtract(_cacheExpirationOffset)); return result; } }