Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs
TekH adc231f10e 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.
2026-09-23 15:32:44 +02:00

143 lines
4.7 KiB
C#

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 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,
IOptions<AuthTokenKeys> authTokenKeyOptions,
IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_authTokenKeys = authTokenKeyOptions.Value;
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
}
/// <inheritdoc/>
public bool IsAuthenticated(string envelopeKey)
{
if (string.IsNullOrWhiteSpace(envelopeKey))
{
_logger.LogWarning("IsAuthenticated called with null or empty envelope key");
return false;
}
var context = _httpContextAccessor.HttpContext;
if (context?.User?.Identity?.IsAuthenticated != true)
{
_logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey);
return false;
}
var sub = GetEnvelopeKeyFromClaims(context.User);
var isValid = sub == envelopeKey;
if (!isValid)
{
_logger.LogWarning(
"Envelope key mismatch: Expected {ExpectedKey}, Got {ActualKey}",
envelopeKey,
sub ?? "(null)");
}
else
{
_logger.LogDebug("User authenticated for envelope {EnvelopeKey}", envelopeKey);
}
return isValid;
}
/// <inheritdoc/>
public string? GetAuthenticatedEnvelopeKey()
{
var context = _httpContextAccessor.HttpContext;
if (context?.User?.Identity?.IsAuthenticated != true)
return null;
return GetEnvelopeKeyFromClaims(context.User);
}
/// <inheritdoc/>
public ClaimsPrincipal? GetCurrentUser()
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext is null)
{
_logger.LogDebug("GetCurrentUser: HttpContext is null (Blazor SignalR circuit without active HTTP request).");
return null;
}
// 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;
}
}