From adc231f10ebab8511b7ae90e85e1e2466b07ec27 Mon Sep 17 00:00:00 2001 From: TekH Date: Wed, 23 Sep 2026 15:32:44 +0200 Subject: [PATCH] fix: resolve sender identity in Blazor Server SignalR circuits JwtBearer middleware does not run for WebSocket upgrade requests, so HttpContext.User is anonymous inside Blazor Server components. Fall back to manual cookie parsing + token validation (same pattern used by EnvelopeReceiverAuthorizationService). Cache the resulting principal on HttpContext.User for subsequent calls within the same circuit. Also inject IOptionsMonitor and IOptions to support the validation path. --- .../Services/EnvelopeAuthService.cs | 99 ++++++++++++++----- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs index 9c2dac20..65ca829e 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs @@ -1,22 +1,35 @@ +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; +using DigitalData.Auth.Claims; +using EnvelopeGenerator.Domain.Constants; +using EnvelopeGenerator.Server.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Options; namespace EnvelopeGenerator.Server.Services; /// -/// Server-side authentication service for envelope receiver access validation. -/// Uses HttpContext to check JWT claims and envelope key authorization. +/// Server-side authentication service for envelope sender access validation. +/// Uses HttpContext to check JWT claims; falls back to manual cookie parsing +/// in Blazor Server SignalR circuits where JwtBearer middleware does not run. /// public class EnvelopeAuthService : IEnvelopeAuthService { private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; + private readonly AuthTokenKeys _authTokenKeys; + private readonly IOptionsMonitor _jwtBearerOptionsMonitor; public EnvelopeAuthService( IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger, + IOptions authTokenKeyOptions, + IOptionsMonitor jwtBearerOptionsMonitor) { _httpContextAccessor = httpContextAccessor; _logger = logger; + _authTokenKeys = authTokenKeyOptions.Value; + _jwtBearerOptionsMonitor = jwtBearerOptionsMonitor; } /// @@ -29,25 +42,21 @@ public class EnvelopeAuthService : IEnvelopeAuthService } var context = _httpContextAccessor.HttpContext; - - // Check if user is authenticated + if (context?.User?.Identity?.IsAuthenticated != true) { _logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey); return false; } - // Get envelope key from claims var sub = GetEnvelopeKeyFromClaims(context.User); - - // Verify envelope key matches var isValid = sub == envelopeKey; - + if (!isValid) { _logger.LogWarning( - "Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}", - envelopeKey, + "Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}", + envelopeKey, sub ?? "(null)"); } else @@ -62,7 +71,7 @@ public class EnvelopeAuthService : IEnvelopeAuthService public string? GetAuthenticatedEnvelopeKey() { var context = _httpContextAccessor.HttpContext; - + if (context?.User?.Identity?.IsAuthenticated != true) return null; @@ -72,20 +81,62 @@ public class EnvelopeAuthService : IEnvelopeAuthService /// public ClaimsPrincipal? GetCurrentUser() { - return _httpContextAccessor.HttpContext?.User; - } - - private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user) - { - // Try NameIdentifier first (standard claim) - var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; - - // Fallback to "sub" claim (JWT standard) - if (string.IsNullOrWhiteSpace(sub)) + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext is null) { - sub = user.FindFirst("sub")?.Value; + _logger.LogDebug("GetCurrentUser: HttpContext is null (Blazor SignalR circuit without active HTTP request)."); + return null; } - return sub; + // Normal HTTP request: JwtBearer middleware already populated HttpContext.User. + if (httpContext.User?.Identity?.IsAuthenticated == true) + return httpContext.User; + + // Blazor Server SignalR circuit: JwtBearer middleware does not run for WebSocket + // upgrade requests, so HttpContext.User is anonymous. Parse the sender JWT from + // the AuthToken cookie manually — same pattern as EnvelopeReceiverAuthorizationService. + if (!httpContext.Request.Cookies.TryGetValue(_authTokenKeys.Cookie, out var token) + || string.IsNullOrWhiteSpace(token)) + { + _logger.LogDebug("GetCurrentUser: '{CookieName}' cookie not found.", _authTokenKeys.Cookie); + return null; + } + + var principal = ValidateSenderToken(token); + if (principal is null) + { + _logger.LogDebug("GetCurrentUser: Sender token validation failed."); + return null; + } + + // Cache on HttpContext.User so subsequent calls in the same circuit are free. + httpContext.User = principal; + + return principal; + } + + private ClaimsPrincipal? ValidateSenderToken(string token) + { + try + { + var parameters = _jwtBearerOptionsMonitor + .Get(AuthScheme.Sender) + .TokenValidationParameters + .Clone(); + + var handler = new JwtSecurityTokenHandler(); + return handler.ValidateToken(token, parameters, out _); + } + catch (Exception ex) + { + _logger.LogDebug(ex, "Sender token validation failed."); + return null; + } + } + + private static string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user) + { + return user.FindFirst(ClaimTypes.NameIdentifier)?.Value + ?? user.FindFirst("sub")?.Value; } }