using System.Net.Http.Headers; using System.Threading; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Http; using Microsoft.Extensions.DependencyInjection; using NexusReader.Application.Abstractions.Services; using NexusReader.UI.Shared.Services; namespace NexusReader.Web.Client.Handlers; /// /// A secure HTTP message delegating handler that automatically appends JWT tokens /// to trusted origin requests and transparently refreshes expired tokens in a thread-safe manner. /// public class AuthenticationHeaderHandler : DelegatingHandler { private readonly INativeStorageService _storageService; private readonly IServiceProvider _serviceProvider; private const string TokenKey = "nexus_auth_token"; private static readonly SemaphoreSlim _refreshSemaphore = new(1, 1); public AuthenticationHeaderHandler(INativeStorageService storageService, IServiceProvider serviceProvider) { _storageService = storageService; _serviceProvider = serviceProvider; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { // Force browser to forward credentials (cookies) for SSR hydration sync request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); var path = request.RequestUri?.AbsolutePath ?? ""; bool isAuthEndpoint = path.Contains("identity/login") || path.Contains("identity/register") || path.Contains("identity/refresh"); // SECURITY FIX (CWE-200): Ensure we only append JWT tokens to local or trusted base origin requests var navigationManager = _serviceProvider.GetRequiredService(); bool isTrustedHost = request.RequestUri != null && (!request.RequestUri.IsAbsoluteUri || request.RequestUri.ToString().StartsWith(navigationManager.BaseUri, StringComparison.OrdinalIgnoreCase)); string? originalToken = null; if (!isAuthEndpoint && isTrustedHost) { var tokenResult = await _storageService.GetSecureString(TokenKey); if (tokenResult.IsSuccess && !string.IsNullOrEmpty(tokenResult.Value)) { originalToken = tokenResult.Value; // Only attach the Bearer token if it is not expired if (!JwtTokenValidator.IsExpired(originalToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", originalToken); } } } var response = await base.SendAsync(request, cancellationToken); // Transparent JWT Auto-Refresh on 401 Unauthorized if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !isAuthEndpoint) { await _refreshSemaphore.WaitAsync(cancellationToken); try { // Re-read token to verify if another concurrent request already refreshed it var tokenResult = await _storageService.GetSecureString(TokenKey); var currentToken = tokenResult.IsSuccess ? tokenResult.Value : null; bool refreshed = false; if (!string.IsNullOrEmpty(currentToken) && currentToken != originalToken) { refreshed = true; } else { // SECURITY FIX (CWE-400): Resolve scoped services within an explicit using scope to prevent memory leaks using var scope = _serviceProvider.CreateScope(); var identityService = scope.ServiceProvider.GetRequiredService(); var refreshResult = await identityService.RefreshTokenAsync(); if (refreshResult.IsSuccess) { var newTokenResult = await _storageService.GetSecureString(TokenKey); currentToken = newTokenResult.IsSuccess ? newTokenResult.Value : null; refreshed = !string.IsNullOrEmpty(currentToken); } else { await identityService.LogoutAsync(); } } if (refreshed && !string.IsNullOrEmpty(currentToken)) { var newRequest = await CloneHttpRequestMessageAsync(request); newRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", currentToken); return await base.SendAsync(newRequest, cancellationToken); } } catch (Exception ex) { // Write standard security audit safe debug log Console.WriteLine($"[AuthHeaderHandler] Automated token renewal failed: {ex.Message}"); } finally { _refreshSemaphore.Release(); } } return response; } private async Task CloneHttpRequestMessageAsync(HttpRequestMessage req) { var clone = new HttpRequestMessage(req.Method, req.RequestUri) { Version = req.Version }; if (req.Content != null) { var ms = new System.IO.MemoryStream(); await req.Content.CopyToAsync(ms); ms.Position = 0; clone.Content = new StreamContent(ms); foreach (var h in req.Content.Headers) { clone.Content.Headers.TryAddWithoutValidation(h.Key, h.Value); } } foreach (var h in req.Headers) { clone.Headers.TryAddWithoutValidation(h.Key, h.Value); } clone.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); return clone; } }