using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text; 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 formContentKeyValues = new List>() { new ("grant_type", "client_credentials"), new ("scopes", string.Join(" ", _scopes)) }; if (options.IsCredentialsInHeader) { formContentKeyValues.AddRange([ new KeyValuePair("client_id", options.ClientId), new KeyValuePair("client_secret", options.ClientSecret) ]); } var formContent = new FormUrlEncodedContent(formContentKeyValues); var request = new HttpRequestMessage(HttpMethod.Post, options.TokenEndpoint) { Content = formContent }; if (false == options.IsCredentialsInHeader) { var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{options.ClientId}:{options.ClientSecret}")); request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); } 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; } }