fix(admin): authenticate SignalR hub clients with a bearer-token scheme
The Admin-003 fix gated every SignalR hub with [Authorize], but the server-side
Blazor HubConnection clients had no way to authenticate: the browser's HttpOnly
auth cookie is not reachable from the interactive circuit, so every hub negotiate
returned 401 and the Admin live-update feature was non-functional app-wide
(silently degraded on Hosts/ScriptLog, fatal on the cluster pages).
Introduce a token-based hub auth path:
- HubTokenService mints/validates short-lived tokens using ASP.NET Core Data
Protection (the same primitive that protects the auth cookie — no signing-key
management, no new packages). Tokens carry the user's name + roles.
- HubTokenAuthenticationHandler is a custom "HubToken" auth scheme that reads the
token from the Authorization: Bearer header (negotiate) or the access_token
query parameter (WebSocket upgrade).
- The "HubClients" authorization policy runs both the cookie and HubToken
schemes; the hub endpoints use RequireAuthorization("HubClients").
- AdminHubConnectionFactory builds hub connections with an AccessTokenProvider
that mints a fresh token for the circuit's authenticated user on every
(re)connect. All six hub-consuming pages now resolve connections through it.
Hub negotiate now returns 200 and the WebSocket upgrades (101); live updates
work. The best-effort try/catch guards added previously are kept as defence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
@inject NodeAclService AclSvc
|
||||
@inject PermissionProbeService ProbeSvc
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
@@ -222,10 +223,7 @@ else
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender || _hub is not null) return;
|
||||
_hub = new HubConnectionBuilder()
|
||||
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet"))
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
_hub = HubFactory.Create("/hubs/fleet");
|
||||
_hub.On<NodeAclChangedMessage>("NodeAclChanged", async msg =>
|
||||
{
|
||||
if (msg.ClusterId != ClusterId || msg.GenerationId != GenerationId) return;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
@inject ClusterService ClusterSvc
|
||||
@inject GenerationService GenerationSvc
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
|
||||
@if (!_loaded)
|
||||
{
|
||||
@@ -179,10 +180,7 @@ else
|
||||
|
||||
private async Task ConnectHubAsync()
|
||||
{
|
||||
_hub = new HubConnectionBuilder()
|
||||
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet"))
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
_hub = HubFactory.Create("/hubs/fleet");
|
||||
|
||||
_hub.On<NodeStateChangedMessage>("NodeStateChanged", async msg =>
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
|
||||
@inject ClusterNodeService NodeSvc
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h4 class="panel-head">Redundancy topology</h4>
|
||||
@@ -130,10 +131,7 @@ else
|
||||
|
||||
private async Task ConnectHubAsync()
|
||||
{
|
||||
_hub = new HubConnectionBuilder()
|
||||
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet"))
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
_hub = HubFactory.Create("/hubs/fleet");
|
||||
|
||||
_hub.On<RoleChangedMessage>("RoleChanged", async msg =>
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
@rendermode RenderMode.InteractiveServer
|
||||
@inject IServiceScopeFactory ScopeFactory
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h1 class="page-title">Driver host status</h1>
|
||||
@@ -155,8 +156,7 @@ else
|
||||
// poll stays as a safety net in case the hub connection is down.
|
||||
private async Task ConnectHubAsync()
|
||||
{
|
||||
var hubUrl = Nav.ToAbsoluteUri("/hubs/fleet");
|
||||
_hub = new HubConnectionBuilder().WithUrl(hubUrl).WithAutomaticReconnect().Build();
|
||||
_hub = HubFactory.Create("/hubs/fleet");
|
||||
_hub.On<ResilienceStatusChangedMessage>("ResilienceStatusChanged", OnResilienceChanged);
|
||||
try
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@inject ClusterService ClusterSvc
|
||||
@inject AclChangeNotifier Notifier
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h1 class="page-title">LDAP group → Admin role grants</h1>
|
||||
@@ -171,10 +172,7 @@ else
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender || _hub is not null) return;
|
||||
_hub = new HubConnectionBuilder()
|
||||
.WithUrl(Nav.ToAbsoluteUri("/hubs/fleet"))
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
_hub = HubFactory.Create("/hubs/fleet");
|
||||
_hub.On<RoleGrantsChangedMessage>("RoleGrantsChanged", async _ =>
|
||||
{
|
||||
await ReloadAsync();
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Hubs
|
||||
@inject NavigationManager Nav
|
||||
@inject AdminHubConnectionFactory HubFactory
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h1 class="page-title">Script log viewer</h1>
|
||||
@@ -120,10 +121,7 @@ else
|
||||
|
||||
try
|
||||
{
|
||||
_hub ??= new HubConnectionBuilder()
|
||||
.WithUrl(Nav.ToAbsoluteUri("/hubs/script-log"))
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
_hub ??= HubFactory.Create("/hubs/script-log");
|
||||
|
||||
if (_hub.State == HubConnectionState.Disconnected)
|
||||
await _hub.StartAsync();
|
||||
|
||||
@@ -12,3 +12,4 @@
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Layout
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Clusters
|
||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||
|
||||
Reference in New Issue
Block a user