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:
@@ -1,22 +1,35 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
using System.Security.Claims;
|
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;
|
namespace EnvelopeGenerator.Server.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Server-side authentication service for envelope receiver access validation.
|
/// Server-side authentication service for envelope sender access validation.
|
||||||
/// Uses HttpContext to check JWT claims and envelope key authorization.
|
/// Uses HttpContext to check JWT claims; falls back to manual cookie parsing
|
||||||
|
/// in Blazor Server SignalR circuits where JwtBearer middleware does not run.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnvelopeAuthService : IEnvelopeAuthService
|
public class EnvelopeAuthService : IEnvelopeAuthService
|
||||||
{
|
{
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly ILogger<EnvelopeAuthService> _logger;
|
private readonly ILogger<EnvelopeAuthService> _logger;
|
||||||
|
private readonly AuthTokenKeys _authTokenKeys;
|
||||||
|
private readonly IOptionsMonitor<JwtBearerOptions> _jwtBearerOptionsMonitor;
|
||||||
|
|
||||||
public EnvelopeAuthService(
|
public EnvelopeAuthService(
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<EnvelopeAuthService> logger)
|
ILogger<EnvelopeAuthService> logger,
|
||||||
|
IOptions<AuthTokenKeys> authTokenKeyOptions,
|
||||||
|
IOptionsMonitor<JwtBearerOptions> jwtBearerOptionsMonitor)
|
||||||
{
|
{
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_authTokenKeys = authTokenKeyOptions.Value;
|
||||||
|
_jwtBearerOptionsMonitor = jwtBearerOptionsMonitor;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@@ -30,17 +43,13 @@ public class EnvelopeAuthService : IEnvelopeAuthService
|
|||||||
|
|
||||||
var context = _httpContextAccessor.HttpContext;
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
|
||||||
// Check if user is authenticated
|
|
||||||
if (context?.User?.Identity?.IsAuthenticated != true)
|
if (context?.User?.Identity?.IsAuthenticated != true)
|
||||||
{
|
{
|
||||||
_logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey);
|
_logger.LogDebug("User is not authenticated for envelope {EnvelopeKey}", envelopeKey);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get envelope key from claims
|
|
||||||
var sub = GetEnvelopeKeyFromClaims(context.User);
|
var sub = GetEnvelopeKeyFromClaims(context.User);
|
||||||
|
|
||||||
// Verify envelope key matches
|
|
||||||
var isValid = sub == envelopeKey;
|
var isValid = sub == envelopeKey;
|
||||||
|
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
@@ -72,20 +81,62 @@ public class EnvelopeAuthService : IEnvelopeAuthService
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ClaimsPrincipal? GetCurrentUser()
|
public ClaimsPrincipal? GetCurrentUser()
|
||||||
{
|
{
|
||||||
return _httpContextAccessor.HttpContext?.User;
|
var httpContext = _httpContextAccessor.HttpContext;
|
||||||
}
|
if (httpContext is null)
|
||||||
|
|
||||||
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
|
||||||
{
|
{
|
||||||
// Try NameIdentifier first (standard claim)
|
_logger.LogDebug("GetCurrentUser: HttpContext is null (Blazor SignalR circuit without active HTTP request).");
|
||||||
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to "sub" claim (JWT standard)
|
// Normal HTTP request: JwtBearer middleware already populated HttpContext.User.
|
||||||
if (string.IsNullOrWhiteSpace(sub))
|
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))
|
||||||
{
|
{
|
||||||
sub = user.FindFirst("sub")?.Value;
|
_logger.LogDebug("GetCurrentUser: '{CookieName}' cookie not found.", _authTokenKeys.Cookie);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return sub;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user