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.
143 lines
4.7 KiB
C#
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;
|
|
}
|
|
}
|