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<JwtBearerOptions> and IOptions<AuthTokenKeys>
to support the validation path.
This commit is contained in:
2026-09-23 15:32:44 +02:00
parent 999fac7dac
commit adc231f10e

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public class EnvelopeAuthService : IEnvelopeAuthService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<EnvelopeAuthService> _logger;
private readonly AuthTokenKeys _authTokenKeys;
private readonly IOptionsMonitor<JwtBearerOptions> _jwtBearerOptionsMonitor;
public EnvelopeAuthService(
IHttpContextAccessor httpContextAccessor,
ILogger<EnvelopeAuthService> logger)
ILogger<EnvelopeAuthService> logger,
IOptions<AuthTokenKeys> authTokenKeyOptions,
IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_authTokenKeys = authTokenKeyOptions.Value;
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
}
/// <inheritdoc/>
@@ -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
/// <inheritdoc/>
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;
}
}