Blazor Server SignalR circuit authorization caching with revoked credentials
0 reputation · 12 May 2020, 20:14 UTC
0 reputation · 12 May 2020, 20:14 UTC
Blazor Server maintains a persistent SignalR circuit for each connected client. When policy-based authorization is applied to components, the authorization decision is evaluated during the initial render and then cached for the lifetime of that circuit. The authentication cookie issued by ASP.NET Core Identity is validated on each hub invocation, but the cookie only carries an expiration timestamp—it does not reflect server-side changes such as account lockout, role removal, or user deletion.
This creates a window where a client holding a still-valid cookie continues to operate with stale privileges until the cookie naturally expires or the circuit is terminated. The framework does not provide a built-in mechanism to push revocation events from the identity store to active circuits, nor does it automatically re-evaluate authorization policies after the initial render.
AuthorizeHubFilter or CircuitHandler reliably call UserManager.GetUserAsync on every invocation to close the stale-privilege gap, and what are the performance implications at scale?35775 reputation · 13 May 2020, 01:24 UTC
To force the re-evaluation of authorization policies on every SignalR hub invocation without tearing down the circuit, you must move the authorization check from the component level (which is cached) to the hub pipeline. The recommended pattern is implementing a custom IHubFilter.
A custom IHubFilter allows you to intercept every hub method call. By injecting IAuthorizationService and UserManager, you can verify the user's current status against the database before the hub method executes.
Performance Implications: Calling UserManager.GetUserAsync on every invocation introduces a database round-trip. At scale, this can create a significant bottleneck. To mitigate this, implement a short-lived cache (e.g., 5–30 seconds) using IMemoryCache to store the user's authorization state, balancing security freshness with database load.
Since the framework does not natively push revocation events, you can implement a manual signaling mechanism. By maintaining a mapping of User IDs to Connection IDs, the server can push a specific message to the client to trigger a redirect.
IHubFilter to perform the check.
public class AuthorizationRevalidationFilter : IHubFilter
{
private readonly IAuthorizationService _authService;
private readonly UserManager<IdentityUser> _userManager;
public AuthorizationRevalidationFilter(IAuthorizationService authService, UserManager<IdentityUser> userManager)
{
_authService = authService;
_userManager = userManager;
}
public async ValueTask<HubInvocationContext> InvokeAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<HubInvocationContext>> next)
{
var user = await _userManager.GetUserAsync(invocationContext.Context.User);
if (user == null)
{
throw new HubException("User no longer exists or is disabled.");
}
var result = await _authService.AuthorizeAsync(invocationContext.Context.User, null, "YourRequiredPolicy");
if (!result.Succeeded)
{
throw new HubException("Authorization revoked.");
}
return await next(invocationContext);
}
}
services.AddSignalR(options =>
{
options.AddFilter<AuthorizationRevalidationFilter>();
});
IHubContext to notify the client:
await _hubContext.Clients.User(userId).SendAsync("ForceLogout");
ForceLogout event by redirecting the user to the login page.
CircuitHandler is unsuitable for per-invocation checks because its methods (like OnConnectionUpAsync) only trigger during circuit lifecycle changes, not during individual hub calls.UserManager logic must be replaced with the corresponding identity provider's validation logic.IHubFilter is available.To refine this recommendation: Are you using a distributed cache (like Redis) for session state, or is the application running on a single server? This determines if IMemoryCache is sufficient or if a distributed cache is required for the re-validation window.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.