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