{"path":"NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs","purpose":"A HTTP DelegatingHandler for the client that appends stored JWTs to trusted requests, forces browser credentials for SSR, and transparently refreshes expired tokens in a thread-safe manner.","classification":{"role":"handler","layer":"infrastructure","confidence":0.9,"evidence":["Integration/client pattern","Application/service path heuristic","class AuthenticationHeaderHandler : DelegatingHandler","appends Authorization header and performs token refresh via IIdentityService"]},"className":"AuthenticationHeaderHandler","methods":[{"name":"SendAsync","line":27,"endLine":110,"signature":"(request: HttpRequestMessage, cancellationToken: CancellationToken) -> Task","purpose":"Intercepts outgoing HTTP requests to add a Bearer token for trusted origins, forwards the request, and on 401 attempts a thread-safe token refresh and optionally retries the request with the new token.","calls":[{"targetFile":"unknown","targetMethod":"INativeStorageService.GetSecureString","callLine":47,"paramSummary":"TokenKey ('nexus_auth_token') to read stored JWT"},{"targetFile":"unknown","targetMethod":"INativeStorageService.GetSecureString","callLine":64,"paramSummary":"TokenKey to re-read token after detecting 401"},{"targetFile":"unknown","targetMethod":"IIdentityService.RefreshTokenAsync","callLine":78,"paramSummary":"no params; triggers refresh flow (resolved from scoped provider)"},{"targetFile":"unknown","targetMethod":"INativeStorageService.GetSecureString","callLine":81,"paramSummary":"TokenKey to read token after refresh"},{"targetFile":"unknown","targetMethod":"IIdentityService.LogoutAsync","callLine":87,"paramSummary":"no params; invoked if refresh fails"},{"targetFile":"self","targetMethod":"CloneHttpRequestMessageAsync","callLine":93,"paramSummary":"original HttpRequestMessage to produce a resendable clone"}],"actions":[{"id":"sendasync_external-call_27_0","kind":"external-call","label":"Calls external dependency","line":27,"detail":"protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)","visibility":"secondary-visible","confidence":0.82},{"id":"mutation_30","kind":"mapping","label":"Force browser credentials for SSR","line":30,"detail":"request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include)","visibility":"detail-only","confidence":0.7},{"id":"branch_32","kind":"branch","label":"Detect auth endpoints","line":32,"conditionSummary":"path contains identity/login|register|refresh","outcomeLabels":["isAuthEndpoint=true","isAuthEndpoint=false"],"visibility":"detail-only","confidence":0.7},{"id":"branch_38","kind":"branch","label":"Trusted host guard","line":38,"conditionSummary":"request.RequestUri is local or starts with NavigationManager.BaseUri","outcomeLabels":["isTrustedHost=true","isTrustedHost=false"],"visibility":"detail-only","confidence":0.7},{"id":"sendasync_branch_45_1","kind":"branch","label":"Evaluates branch condition","line":45,"detail":"if (!isAuthEndpoint && isTrustedHost)","conditionSummary":"!isAuthEndpoint && isTrustedHost","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"external-call_47","kind":"external-call","label":"Read stored token","line":47,"detail":"Calls INativeStorageService.GetSecureString to obtain JWT","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_47_2","kind":"await","label":"Waits for async work","line":47,"detail":"var tokenResult = await _storageService.GetSecureString(TokenKey);","visibility":"secondary-visible","confidence":0.81},{"id":"guard-clause_48","kind":"guard-clause","label":"Attach Authorization header","line":48,"conditionSummary":"token read succeeded and non-empty","outcomeLabels":["attach-bearer","skip-attachment"],"visibility":"detail-only","confidence":0.7},{"id":"sendasync_branch_48_3","kind":"branch","label":"Evaluates branch condition","line":48,"detail":"if (tokenResult.IsSuccess && !string.IsNullOrEmpty(tokenResult.Value))","conditionSummary":"tokenResult.IsSuccess && !string.IsNullOrEmpty(tokenResult.Value)","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"sendasync_external-call_55_4","kind":"external-call","label":"Calls external dependency","line":55,"detail":"var response = await base.SendAsync(request, cancellationToken);","visibility":"secondary-visible","confidence":0.82},{"id":"external-call_55","kind":"external-call","label":"Send base HTTP request","line":55,"detail":"Delegates to base.SendAsync to perform network call","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_55_5","kind":"await","label":"Waits for async work","line":55,"detail":"var response = await base.SendAsync(request, cancellationToken);","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_branch_58_6","kind":"branch","label":"Evaluates branch condition","line":58,"detail":"if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !isAuthEndpoint)","conditionSummary":"response.StatusCode == System.Net.HttpStatusCode.Unauthorized && !isAuthEndpoint","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"branch_58","kind":"branch","label":"Handle 401 Unauthorized","line":58,"conditionSummary":"response.StatusCode == Unauthorized && not an auth endpoint","outcomeLabels":["attempt-refresh","return-response"],"visibility":"detail-only","confidence":0.7},{"id":"synchronization_60","kind":"mapping","label":"Semaphore for concurrent refresh","line":60,"detail":"WaitAsync/Release on static _refreshSemaphore to serialize refresh attempts","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_60_7","kind":"await","label":"Waits for async work","line":60,"detail":"await _refreshSemaphore.WaitAsync(cancellationToken);","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_try_61_8","kind":"try","label":"Begins protected execution","line":61,"detail":"try","visibility":"primary-visible","confidence":0.84},{"id":"sendasync_await_64_9","kind":"await","label":"Waits for async work","line":64,"detail":"var tokenResult = await _storageService.GetSecureString(TokenKey);","visibility":"secondary-visible","confidence":0.81},{"id":"branch_69","kind":"branch","label":"Detect token already refreshed by another request","line":69,"conditionSummary":"currentToken != null && currentToken != originalToken","outcomeLabels":["use-new-token","perform-refresh"],"visibility":"detail-only","confidence":0.7},{"id":"sendasync_branch_69_10","kind":"branch","label":"Evaluates branch condition","line":69,"detail":"if (!string.IsNullOrEmpty(currentToken) && currentToken != originalToken)","conditionSummary":"!string.IsNullOrEmpty(currentToken) && currentToken != originalToken","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"sendasync_fallback_73_11","kind":"fallback","label":"Falls back to alternate path","line":73,"detail":"else","outcomeLabels":["fallback"],"visibility":"primary-visible","confidence":0.84},{"id":"scope_76","kind":"mapping","label":"Create service scope for scoped services","line":76,"detail":"using var scope = _serviceProvider.CreateScope(); resolves IIdentityService from scope","visibility":"detail-only","confidence":0.7},{"id":"external-call_78","kind":"external-call","label":"Refresh token via identity service","line":78,"detail":"identityService.RefreshTokenAsync(); if success, re-read stored token","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_78_12","kind":"await","label":"Waits for async work","line":78,"detail":"var refreshResult = await identityService.RefreshTokenAsync();","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_branch_79_13","kind":"branch","label":"Evaluates branch condition","line":79,"detail":"if (refreshResult.IsSuccess)","conditionSummary":"refreshResult.IsSuccess","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"branch_79","kind":"branch","label":"Refresh outcome","line":79,"conditionSummary":"refreshResult.IsSuccess","outcomeLabels":["refreshed=true (read token)","refreshed=false (logout)"],"visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_81_14","kind":"await","label":"Waits for async work","line":81,"detail":"var newTokenResult = await _storageService.GetSecureString(TokenKey);","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_fallback_85_15","kind":"fallback","label":"Falls back to alternate path","line":85,"detail":"else","outcomeLabels":["fallback"],"visibility":"primary-visible","confidence":0.84},{"id":"external-call_87","kind":"external-call","label":"Logout on refresh failure","line":87,"detail":"identityService.LogoutAsync()","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_87_16","kind":"await","label":"Waits for async work","line":87,"detail":"await identityService.LogoutAsync();","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_branch_91_17","kind":"branch","label":"Evaluates branch condition","line":91,"detail":"if (refreshed && !string.IsNullOrEmpty(currentToken))","conditionSummary":"refreshed && !string.IsNullOrEmpty(currentToken)","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"control-flow_91","kind":"mapping","label":"Retry original request with refreshed token","line":91,"detail":"Clones request, sets Authorization with currentToken, and resends via base.SendAsync","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_93_18","kind":"await","label":"Waits for async work","line":93,"detail":"var newRequest = await CloneHttpRequestMessageAsync(request);","visibility":"secondary-visible","confidence":0.81},{"id":"sendasync_external-call_95_19","kind":"external-call","label":"Calls external dependency","line":95,"detail":"return await base.SendAsync(newRequest, cancellationToken);","visibility":"secondary-visible","confidence":0.82},{"id":"sendasync_return_95_21","kind":"return","label":"Returns result","line":95,"detail":"return await base.SendAsync(newRequest, cancellationToken);","visibility":"detail-only","confidence":0.7},{"id":"sendasync_await_95_20","kind":"await","label":"Waits for async work","line":95,"detail":"return await base.SendAsync(newRequest, cancellationToken);","visibility":"secondary-visible","confidence":0.81},{"id":"error-handling_98","kind":"mapping","label":"Catch and audit log refresh exceptions","line":98,"detail":"Console.WriteLine with sanitized message","visibility":"detail-only","confidence":0.7},{"id":"sendasync_catch_98_22","kind":"catch","label":"Handles exception path","line":98,"detail":"catch (Exception ex)","conditionSummary":"Exception ex","outcomeLabels":["handled exception"],"visibility":"primary-visible","confidence":0.86},{"id":"sendasync_finally_103_23","kind":"finally","label":"Runs cleanup or finalization","line":103,"detail":"finally","visibility":"secondary-visible","confidence":0.84},{"id":"synchronization_105","kind":"mapping","label":"Release semaphore","line":105,"detail":"_refreshSemaphore.Release() in finally","visibility":"detail-only","confidence":0.7},{"id":"sendasync_return_109_24","kind":"return","label":"Returns result","line":109,"detail":"return response;","visibility":"detail-only","confidence":0.7}]},{"name":"CloneHttpRequestMessageAsync","line":112,"endLine":140,"signature":"(req: HttpRequestMessage) -> Task","purpose":"Creates a deep clone of an HttpRequestMessage (including content stream and headers) suitable for resending.","calls":[],"actions":[{"id":"construction_114","kind":"mapping","label":"Create new HttpRequestMessage clone","line":114,"detail":"new HttpRequestMessage(req.Method, req.RequestUri) with Version set","visibility":"detail-only","confidence":0.7},{"id":"branch_119","kind":"branch","label":"Copy content if present","line":119,"conditionSummary":"req.Content != null","outcomeLabels":["copy-content","no-content"],"visibility":"detail-only","confidence":0.7},{"id":"clonehttprequestmessageasync_branch_119_0","kind":"branch","label":"Evaluates branch condition","line":119,"detail":"if (req.Content != null)","conditionSummary":"req.Content != null","outcomeLabels":["true","false"],"visibility":"secondary-visible","confidence":0.78},{"id":"io_121","kind":"mapping","label":"Copy content stream","line":121,"detail":"req.Content.CopyToAsync into MemoryStream, create StreamContent from it","visibility":"detail-only","confidence":0.7},{"id":"clonehttprequestmessageasync_await_122_1","kind":"await","label":"Waits for async work","line":122,"detail":"await req.Content.CopyToAsync(ms);","visibility":"secondary-visible","confidence":0.81},{"id":"loop_126","kind":"loop","label":"Copy content headers","line":126,"detail":"foreach over req.Content.Headers and TryAddWithoutValidation onto clone.Content.Headers","visibility":"detail-only","confidence":0.7},{"id":"clonehttprequestmessageasync_loop_126_2","kind":"loop","label":"Repeats work over a collection or condition","line":126,"detail":"foreach (var h in req.Content.Headers)","conditionSummary":"var h in req.Content.Headers","loopTargetLine":126,"loopExitSummary":"Leaves the loop when the condition no longer holds.","visibility":"primary-visible","confidence":0.86},{"id":"loop_132","kind":"loop","label":"Copy request headers","line":132,"detail":"foreach over req.Headers and TryAddWithoutValidation onto clone.Headers","visibility":"detail-only","confidence":0.7},{"id":"clonehttprequestmessageasync_loop_132_3","kind":"loop","label":"Repeats work over a collection or condition","line":132,"detail":"foreach (var h in req.Headers)","conditionSummary":"var h in req.Headers","loopTargetLine":132,"loopExitSummary":"Leaves the loop when the condition no longer holds.","visibility":"primary-visible","confidence":0.86},{"id":"mutation_137","kind":"mapping","label":"Preserve browser credentials","line":137,"detail":"clone.SetBrowserRequestCredentials(BrowserRequestCredentials.Include)","visibility":"detail-only","confidence":0.7},{"id":"clonehttprequestmessageasync_return_139_4","kind":"return","label":"Returns result","line":139,"detail":"return clone;","visibility":"detail-only","confidence":0.7}]}],"types":[{"name":"AuthenticationHeaderHandler","kind":"model","line":14,"purpose":"Delegating handler that secures outgoing HTTP calls by attaching and refreshing JWTs.","fields":[{"name":"_storageService","type":"INativeStorageService","required":true,"line":16,"description":"Abstraction to read/write secure local storage (used for stored JWT)"},{"name":"_serviceProvider","type":"IServiceProvider","required":true,"line":17,"description":"Used to resolve NavigationManager and scoped IIdentityService when refreshing tokens"},{"name":"TokenKey","type":"string (const)","required":true,"line":18,"description":"Key name used in secure storage for JWT"},{"name":"_refreshSemaphore","type":"SemaphoreSlim (static)","required":true,"line":19,"description":"Serializes concurrent token refresh attempts"}]}],"serviceRegistrations":[],"startupActions":[],"dependencies":["NexusReader.Application.Abstractions.Services (INativeStorageService, IIdentityService)","Microsoft.AspNetCore.Components (NavigationManager)","IServiceProvider (DI resolution)"],"patterns":["HTTP DelegatingHandler","Token auto-refresh","Scoped resolution for short-lived services","Concurrency serialization (Semaphore)"],"domainConcepts":["Authentication","JWT","Token refresh","Secure storage"],"keyDetails":"Appends JWT only for trusted origins and non-auth endpoints; transparently attempts refresh on 401 using a semaphore to avoid duplicate refreshes; resolves IIdentityService in an explicit scope and will call LogoutAsync on refresh failure; clones HttpRequestMessage to safely retry requests with new token.","orchestrationMethods":[{"name":"SendAsync","line":27,"confidence":0.98,"reason":"Coordinates 6 downstream calls with 20 architectural actions.","actionKinds":["external-call","mapping","branch","await","guard-clause","try","fallback","return","catch","finally"],"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs","unknown","unknown","unknown","unknown","unknown"]},{"name":"CloneHttpRequestMessageAsync","line":112,"confidence":0.98,"reason":"Contains 6 architectural actions relevant to business execution.","actionKinds":["mapping","branch","await","loop","return"],"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]}],"typedContracts":[{"name":"AuthenticationHeaderHandler","kind":"model","line":14,"fieldCount":4,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]}],"persistenceInteractions":[],"externalInteractions":[{"methodName":"SendAsync","line":27,"kind":"external-call","detail":"protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":47,"kind":"external-call","detail":"Calls INativeStorageService.GetSecureString to obtain JWT","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":55,"kind":"external-call","detail":"var response = await base.SendAsync(request, cancellationToken);","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":55,"kind":"external-call","detail":"Delegates to base.SendAsync to perform network call","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":78,"kind":"external-call","detail":"identityService.RefreshTokenAsync(); if success, re-read stored token","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":87,"kind":"external-call","detail":"identityService.LogoutAsync()","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"methodName":"SendAsync","line":95,"kind":"external-call","detail":"return await base.SendAsync(newRequest, cancellationToken);","evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]}],"evidenceAnchors":[{"kind":"orchestration-method","label":"SendAsync","line":27,"summary":"Coordinates 6 downstream calls with 20 architectural actions.","confidence":0.98,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs","unknown","unknown","unknown","unknown","unknown"]},{"kind":"orchestration-method","label":"CloneHttpRequestMessageAsync","line":112,"summary":"Contains 6 architectural actions relevant to business execution.","confidence":0.98,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"kind":"typed-contract","label":"AuthenticationHeaderHandler","line":14,"summary":"model with 4 fields.","confidence":0.8,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"kind":"external-call","label":"SendAsync","line":27,"summary":"protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)","confidence":0.8,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"kind":"external-call","label":"SendAsync","line":47,"summary":"Calls INativeStorageService.GetSecureString to obtain JWT","confidence":0.8,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]},{"kind":"external-call","label":"SendAsync","line":55,"summary":"var response = await base.SendAsync(request, cancellationToken);","confidence":0.8,"evidencePaths":["NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs"]}],"cacheMetadata":{"schemaVersion":2,"analysisVersion":"2026-05-23.cache-v1","contentChecksum":"608093811122a6aa5e4f93db342a264e1b36b27b57217198aee8536ca8a978ea","sourceByteSize":5981,"analyzedAt":"2026-05-23T16:45:23.948Z","technology":"dotnet"}}