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; } }