using System.Net.Http.Headers; using System.Net.Http.Json; using CM.Authentication.Abstractions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace CM.Authentication; public class JwtTokenManagerService(IOptions options, IHttpClientFactory httpClientFactory, IMemoryCache? memoryCache, ILogger? logger) : IJwtTokenManagerService { private readonly AuthenticationOptions _options = options.Value; private readonly JwtTokenCacheOptions _cacheOptions = options.Value.CacheOptions; private readonly HttpClient _httpClient = httpClientFactory.CreateClient(); private readonly TimeSpan _cacheExpirationOffset = TimeSpan.FromSeconds(options.Value.CacheOptions.ExpirationOffset); public async Task GetTokenAsync(CancellationToken cancellationToken = default) { if (memoryCache != null) { var memoryGetValueResult = memoryCache.TryGetValue(_cacheOptions.CacheKey, out JwtTokenResult? cachedToken); if (memoryGetValueResult && null != cachedToken) { return cachedToken; } } var formContentKeyValues = new List>() { new ("grant_type", "password"), new ("username", _options.Username), new ("password", _options.Password) }; var formContent = new FormUrlEncodedContent(formContentKeyValues); 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 is null) throw new InvalidOperationException("Failed to deserialize the token response content."); var parsedResult = Enum.TryParse(tokenResponse.TokenType, true, 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 JwtTokenResult { AccessToken = tokenResponse.AccessToken, TokenType = tokenType, Accounts = tokenResponse.Accounts, RefreshToken = tokenResponse.RefreshToken, ExpiresIn = tokenResponse.ExpiresIn, ExpiresAt = expiresAt, }; 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; } }