Add SignalR Auth Refresh support to server and .NET client - #67111
Conversation
Server-side: - Add /refresh HTTP endpoint mapped alongside /negotiate - Add tokenLifetimeSeconds to negotiate response (NegotiationResponse + NegotiateProtocol) - Add EnableAuthRefresh and AuthRefreshGracePeriod to HttpConnectionDispatcherOptions - Add UpdateUser method to HttpConnectionContext for updating ClaimsPrincipal - Add TryGetConnectionByConnectionId to HttpConnectionManager - Server re-authenticates on /refresh and updates connection's User and auth expiration - Compute TTL from AuthenticationProperties.ExpiresUtc .NET Client-side: - Add IAuthRefreshFeature interface in Connections.Abstractions - HttpConnection implements IAuthRefreshFeature, POSTs to /refresh endpoint - HubConnection.RefreshAuthAsync() discovers IAuthRefreshFeature via Features collection - Auto-refresh timer schedules at: now + TTL - RefreshBeforeExpiration (default 5 min) - Timer disposes on StopAsync/DisposeAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eFeature, Hub.OnAuthRefreshedAsync, abort on identifier change - New public IConnectionUserUpdateFeature in Connections.Abstractions exposes a UserUpdated event raised by HttpConnectionContext.UpdateUser. - HubConnectionContext.User now reads from IConnectionUserFeature on every access so refreshed claims take effect immediately (including for [Authorize]). - New virtual Hub.OnAuthRefreshedAsync(ClaimsPrincipal? previousUser) lifecycle hook; dispatched by HubConnectionHandler via DefaultHubDispatcher (same scope/activator/activity pattern as OnConnected). - HubConnectionHandler subscribes to IConnectionUserUpdateFeature.UserUpdated; if the recomputed UserIdentifier changes it logs a warning and aborts the connection (SignalR user-targeting requires a stable identifier), otherwise it dispatches OnAuthRefreshedAsync. - Adds tests for HttpConnectionContext exposing the feature, the event firing, exception isolation in the handler, hub User reflecting refreshed claims, OnAuthRefreshedAsync being invoked, and abort-on-identifier-change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
DefaultHubDispatcher.OnAuthRefreshedAsync now acquires the per-connection ChannelBasedSemaphore (same path normal hub invocations use), so the refresh callback respects MaximumParallelInvocations and orders with any in-flight invocations instead of running concurrently. Exceptions from user code are caught and logged via FailedInvokingHubMethod (must not throw out of the semaphore callback). Adds a test that holds the semaphore with a blocking hub method and asserts OnAuthRefreshedAsync only runs after the method releases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added 5 hub-layer tests (same-identifier dispatch, exception in OnAuthRefreshedAsync, multiple sequential refreshes, Context.User reflects new principal, missing feature is no-op) and 3 connection-layer tests (multiple subscribers, unsubscribe, no subscribers). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the previousUser parameter from Hub.OnAuthRefreshedAsync and the IConnectionUserUpdateFeature.UserUpdated event signature. Holding a reference to the previous ClaimsPrincipal across the refresh boundary is unsafe when the identity is a WindowsIdentity, since its underlying SafeHandle can be disposed when the refresh HTTP request completes. Add two tests exercising claim-based authorization through a refresh: - RefreshAddingRequiredClaimAllowsAuthorizedHubMethod - RefreshRemovingRequiredClaimBlocksAuthorizedHubMethod Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…dispose owned identities When /refresh hands the connection a new ClaimsPrincipal backed by a WindowsIdentity, the SafeHandles inside it are tied to the /refresh HTTP request lifetime, not the connection. After that request ends the handles get disposed and any later hub method or OnAuthRefreshedAsync invocation that reaches through to the WindowsIdentity fails. Mirror HttpConnectionDispatcher.CloneUser: clone WindowsIdentity-bearing principals into a connection-owned copy inside UpdateUser, and dispose the previously-owned identities only after the UserUpdated event has been raised so subscribers see the new principal first. Track ownership explicitly via _ownsUserIdentities so DisposeAsync uses the same rule (replacing the prior long-polling-only check). Add three tests covering: clone-on-refresh disposes the prior owned WindowsIdentity; non-WindowsIdentity principals pass through without cloning; DisposeAsync disposes a refresh-clone even outside long polling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ned permission policy Introduce HttpConnectionDispatcherOptions.OnAuthRefresh, a Func<AuthRefreshContext, ValueTask<bool>> the application can supply to inspect the previous and new ClaimsPrincipal during a /refresh call and accept or reject the swap. When the callback returns false the endpoint responds with HTTP 403 (permission_change_rejected) and the connection's user is left unchanged. When null (default) behavior is unchanged - any successful re-auth is accepted. The AuthRefreshContext exposes HttpContext, ConnectionId, PreviousUser, NewUser, NewExpiration, and a mutable DenyReason that surfaces in the 403 body for client diagnostics. Keeps the framework unopinionated: apps decide downgrade-only / protected-claim / subject-mismatch rules without us shipping comparison semantics. Added 4 tests covering: callback receives correct context and accepts, false return produces 403 with DenyReason and leaves user untouched, default description when DenyReason is null, callback exceptions propagate without mutating connection state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…resh endpoint Mirror the existing MapConnectionHandlerEndPointRoutingAppliesNegotiateMetadata tests for the new /refresh endpoint: - when EnableAuthRefresh=true the route table contains three endpoints in order (/negotiate, /refresh, /), and only /refresh carries AuthRefreshMetadata plus HttpConnectionDispatcherOptions - when EnableAuthRefresh is left default only /negotiate and / are registered and neither carries AuthRefreshMetadata Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds HttpConnectionTests.RefreshAuth.cs covering:
- throws InvalidOperationException before connection started
- POSTs to {url}/refresh?id={connectionToken} with correct path composition
- sends Bearer token from AccessTokenProvider
- omits Authorization header when no token provider
- returns parsed tokenLifetimeSeconds (and null when absent)
- throws HttpRequestException on 401/403/404
- propagates network exceptions
- propagates cancellation via CancellationToken
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fresh feature-only auth - Run the auth-refresh path on the Long Polling poll endpoint so token expiration/principal updates stay consistent across transports, with stale-guard handling for concurrent polls. - Rekey connections in the hub lifetime managers (default and Redis) when a refresh changes the UserIdentifier, including scaleout coverage in the Specification.Tests base class. - /refresh now reads only IAuthenticateResultFeature (no AuthenticateAsync fallback), matching negotiate and the connect/poll paths. - Add OnAuthRefresh permission-change handling and functional/unit test coverage (client HubConnectionTests.AuthRefresh, dispatcher, Redis e2e). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Treat WindowsIdentity-backed principals as non-refreshable for auth refresh and omit refresh TTLs for Windows auth. Trim /refresh success responses to only tokenLifetimeSeconds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
fa2c737 to
f074e92
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Seal the refresh metadata marker, remove fallback refresh scheduling, use generic refresh errors, validate refresh option durations, and use semi-auto properties where possible. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make refreshed user updates always skip older authentication expirations and simplify the long-polling principal comparison to avoid array allocation and sorting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Run stateful WebSocket reconnect credentials through the authentication-refresh path so hub auth state updates consistently with refresh and long polling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove the public AuthenticationRefreshGracePeriod API and related delayed-close behavior, and clean up the reconnect refresh principal nullability path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Allow the user-identifier-change refresh test to tolerate the expected race where connection close is observed before the manual refresh completes, and revert JwtSample local testing changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds end-to-end SignalR authentication refresh support so an already-established connection can update its authenticated principal (and associated auth expiration) without reconnecting. This integrates across HTTP connection endpoints (/refresh + negotiate metadata), the SignalR hub layer (caller context snapshotting + refresh notifications), and the .NET client (manual + automatic refresh support).
Changes:
- Server: introduce
/refreshendpoint +EnableAuthenticationRefresh/OnAuthenticationRefreshoptions, and negotiate metadata (tokenLifetimeSeconds) for token lifetime. - Hub layer: apply refreshed principals safely (snapshot
HubCallerContext.User, rejectUserIdentifierchanges by aborting), and addHub.OnAuthenticationRefreshedAsync(). - .NET client: add
HubConnection.RefreshAuthenticationAsync()and auto-refresh scheduling viaAuthenticationRefreshOptions, plus transport support for issuing /refresh with a freshly fetched access token.
Show a summary per file
| File | Description |
|---|---|
| src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs | Capture user identifier when subscribing/unsubscribing to avoid races with refreshed user state. |
| src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/MapSignalRTests.cs | Tests that auth metadata is applied to the new /refresh endpoint. |
| src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/HubConnectionHandlerTests.AuthenticationRefresh.cs | New hub-layer tests for user refresh propagation, serialization, and abort-on-identifier-change behavior. |
| src/SignalR/server/SignalR/test/Microsoft.AspNetCore.SignalR.Tests/DefaultHubLifetimeManagerTests.cs | Minor formatting-only change. |
| src/SignalR/server/Core/src/PublicAPI.Unshipped.txt | Public API tracking for new Hub.OnAuthenticationRefreshedAsync(). |
| src/SignalR/server/Core/src/Internal/SignalRServerActivitySource.cs | Add activity name for auth refresh hub event. |
| src/SignalR/server/Core/src/Internal/HubDispatcher.cs | Add dispatcher entrypoint for auth refresh. |
| src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs | Invoke OnAuthenticationRefreshedAsync, and thread hub-caller-context snapshots through invocations/authorization. |
| src/SignalR/server/Core/src/Internal/DefaultHubCallerContext.cs | Change User to a snapshot principal rather than live connection principal. |
| src/SignalR/server/Core/src/HubConnectionHandlerLog.cs | Add log event for user identifier change during refresh. |
| src/SignalR/server/Core/src/HubConnectionHandler.cs | Subscribe to IConnectionUserRefreshFeature and apply refreshed user state + dispatch hub refresh callback. |
| src/SignalR/server/Core/src/HubConnectionContext.cs | Implement snapshotting and safe user-id computation for refresh; publish updated hub caller context atomically. |
| src/SignalR/server/Core/src/Hub.cs | Add overridable OnAuthenticationRefreshedAsync() hook. |
| src/SignalR/samples/JwtClientSample/Program.cs | Sample updated to demonstrate auth refresh (currently contains debug/placeholder code). |
| src/SignalR/common/Http.Connections/test/NegotiateProtocolTests.cs | Add tests for tokenLifetimeSeconds serialization/parsing. |
| src/SignalR/common/Http.Connections/test/MapConnectionHandlerTests.cs | Add endpoint-routing tests for /refresh endpoint registration/metadata. |
| src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.cs | Make test class partial to split auth-refresh tests into a new file. |
| src/SignalR/common/Http.Connections/test/HttpConnectionDispatcherTests.AuthenticationRefresh.cs | Add extensive dispatcher tests for refresh endpoint, long polling, stateful reconnect, and callback semantics. |
| src/SignalR/common/Http.Connections/src/PublicAPI.Unshipped.txt | Public API tracking for dispatcher options + refresh metadata/context types. |
| src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs | Add log event for refresh rejection by callback. |
| src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs | Implement /refresh endpoint and unify refresh logic with long polling/stateful reconnect user updates. |
| src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs | Add IConnectionUserRefreshFeature and atomic UpdateUser + callback notification + identity ownership handling. |
| src/SignalR/common/Http.Connections/src/HttpConnectionDispatcherOptions.cs | Add public refresh options (EnableAuthenticationRefresh, OnAuthenticationRefresh) and modernize backing fields. |
| src/SignalR/common/Http.Connections/src/ConnectionEndpointRouteBuilderExtensions.cs | Map /refresh endpoint when enabled and stamp refresh metadata/options. |
| src/SignalR/common/Http.Connections/src/AuthenticationRefreshMetadata.cs | New marker metadata type for /refresh endpoint identification. |
| src/SignalR/common/Http.Connections/src/AuthenticationRefreshContext.cs | New context passed to OnAuthenticationRefresh callback. |
| src/SignalR/common/Http.Connections.Common/src/PublicAPI.Unshipped.txt | Public API tracking for NegotiationResponse.TokenLifetime. |
| src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs | Add TokenLifetime to carry server token TTL to clients. |
| src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs | Serialize/parse tokenLifetimeSeconds in negotiate responses. |
| src/SignalR/clients/csharp/Http.Connections.Client/src/Internal/AccessTokenHttpMessageHandler.cs | Add refresh-specific token fetching/caching semantics keyed off request flags. |
| src/SignalR/clients/csharp/Http.Connections.Client/src/HttpConnection.cs | Implement IAuthenticationRefreshFeature and POST /refresh with token lifetime parsing. |
| src/SignalR/clients/csharp/Client/test/UnitTests/HubConnectionTests.AuthenticationRefresh.cs | Unit tests for HubConnection refresh APIs and auto-refresh scheduling behavior. |
| src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.AuthenticationRefresh.cs | Unit tests ensuring /refresh uses freshly fetched tokens and updates cached tokens correctly. |
| src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs | Add JWT policies/endpoints and enable auth refresh for functional tests. |
| src/SignalR/clients/csharp/Client/test/FunctionalTests/Hubs.cs | Add a hub used to validate refresh + authorization changes. |
| src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs | Add clarifying comment around user id provider behavior. |
| src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.AuthenticationRefresh.cs | Functional tests covering refresh, auth changes, reconnect, stateful reconnect scenarios. |
| src/SignalR/clients/csharp/Client/test/FunctionalTests/HeaderUserIdProvider.cs | Prefer NameIdentifier from refreshed principal for user id derivation. |
| src/SignalR/clients/csharp/Client.Core/src/PublicAPI.Unshipped.txt | Public API tracking for client refresh APIs/options. |
| src/SignalR/clients/csharp/Client.Core/src/HubConnectionBuilderExtensions.cs | Add WithAuthenticationRefresh(...) builder extension. |
| src/SignalR/clients/csharp/Client.Core/src/HubConnection.Log.cs | Add client logs for refresh lifecycle + callback failures. |
| src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs | Implement RefreshAuthenticationAsync() and auto-refresh timer scheduling. |
| src/SignalR/clients/csharp/Client.Core/src/AuthenticationRefreshOptions.cs | New options + callback context types for client refresh behavior. |
| src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.1/PublicAPI.Unshipped.txt | Public API tracking for new connection features. |
| src/Servers/Connections.Abstractions/src/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt | Public API tracking for new connection features. |
| src/Servers/Connections.Abstractions/src/PublicAPI/net462/PublicAPI.Unshipped.txt | Public API tracking for new connection features. |
| src/Servers/Connections.Abstractions/src/PublicAPI/net11.0/PublicAPI.Unshipped.txt | Public API tracking for new connection features. |
| src/Servers/Connections.Abstractions/src/PublicAPI/net10.0/PublicAPI.Unshipped.txt | Public API tracking for new connection features. |
| src/Servers/Connections.Abstractions/src/Features/IConnectionUserRefreshFeature.cs | New feature to notify when a connection’s user is refreshed. |
| src/Servers/Connections.Abstractions/src/Features/IAuthenticationRefreshFeature.cs | New feature for initiating auth refresh and exposing initial token lifetime. |
Copilot's findings
- Files reviewed: 50/50 changed files
- Comments generated: 6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
API Proposal for (back) reference: #67226 |
halter73
left a comment
There was a problem hiding this comment.
Another thing to consider can refresh work for really-short-lived (sub 30 second) tokens. Do we care? Short-lived tokens are becoming more common.
I'm approving knowing we want to get this out in preview6 so people can test it out, and that we still need to go through API review and all that.
| return; | ||
| } | ||
|
|
||
| connection.ApplyUserState(user, newUserId); |
There was a problem hiding this comment.
This isn't monotonic the way HttpConnectionContext.UpdateUser is. We recompute newUserId from the captured user instead of the current connection.User, and PublishHubCallerContext is an unconditional Interlocked.Exchange. So if /refresh races a long polling poll and the older refresh takes this lock second, connection.User ends up newer than Context.User and the hub keeps authorizing on the stale principal until the next refresh. Should we thread the expiration through here and drop the apply if it's older, like the connection layer does?
| refreshIn = minimumRefreshInterval; | ||
| } | ||
|
|
||
| _authRefreshTimer = new Timer( |
There was a problem hiding this comment.
Nit: This disposes and reassigns _authRefreshTimer with no lock, and StopAsyncCore nulls it outside the connection lock too. RefreshAuthenticationAsync is public and the timer callback both reschedule through here, so a reschedule racing stop can re-arm the timer after stop nulled it, and two reschedules can leak a Timer. The state guard stops real damage, but we'd still fire once after stop and log a spurious failure. It might be worth protecting the assignments with the connection lock some additional synchronization.
|
/backport to release/11.0-preview6 |
|
Started backporting to |
|
@BrennanConroy backporting to git am output$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch
Applying: Rough draft: SignalR auth token refresh (Option A)
Applying: fixups and sample app
Applying: SignalR Hub-layer auth refresh: drop User cache, IConnectionUserUpdateFeature, Hub.OnAuthRefreshedAsync, abort on identifier change
Applying: Serialize Hub.OnAuthRefreshedAsync through ActiveInvocationLimit
Applying: SignalR auth refresh: expand test coverage
Applying: SignalR auth refresh: drop previousUser, add policy-based tests
Applying: SignalR auth refresh: clone WindowsIdentity on UpdateUser; track and dispose owned identities
Applying: SignalR auth refresh: add OnAuthRefresh callback for application-defined permission policy
Applying: SignalR auth refresh: test AuthRefreshMetadata is wired onto the /refresh endpoint
Applying: SignalR auth refresh: client HttpConnection.RefreshAuthAsync unit tests
Applying: SignalR auth refresh: LP poll-path refresh, UserIdentifier rekey, /refresh feature-only auth
Applying: SignalR auth refresh refinements
Using index info to reconstruct a base tree...
M src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs
M src/SignalR/server/Core/src/HubConnectionContext.cs
M src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
Falling back to patching base and 3-way merge...
Auto-merging src/SignalR/clients/csharp/Client.Core/src/HubConnection.cs
Auto-merging src/SignalR/server/Core/src/HubConnectionContext.cs
Auto-merging src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
CONFLICT (content): Merge conflict in src/SignalR/server/Core/src/Internal/DefaultHubDispatcher.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0012 SignalR auth refresh refinements
Error: The process '/usr/bin/git' failed with exit code 128 |
…67400) Server-side: - Add /refresh HTTP endpoint mapped alongside /negotiate - Add tokenLifetimeSeconds to negotiate response (NegotiationResponse + NegotiateProtocol) - Server re-authenticates on /refresh and updates connection's User and auth expiration - Compute TTL from AuthenticationProperties.ExpiresUtc - New virtual Hub.OnAuthRefreshedAsync() lifecycle hook .NET Client-side: - Add IAuthRefreshFeature interface in Connections.Abstractions - HttpConnection implements IAuthRefreshFeature, POSTs to /refresh endpoint - HubConnection.RefreshAuthAsync() discovers IAuthRefreshFeature via Features collection - Auto-refresh timer schedules at: now + TTL - RefreshBeforeExpiration (default 5 min) (cherry picked from commit 8835dfc)
Summary
Adds SignalR auth refresh support so clients can update authentication credentials for an active connection without reconnecting.
This is primarily for bearer-token scenarios where the access token used to establish the connection expires or rotates while the SignalR connection is still active (although cookies should also work assuming the cookie jar is updated). The feature adds a server refresh endpoint, client refresh APIs/options, negotiate metadata for token lifetime, and hub-layer user state updates so refreshed credentials can be reflected in authorization and user-targeted routing.
High-level flow
HttpConnectionDispatcherOptions.EnableAuthRefresh.tokenLifetimeSeconds.HubConnection.RefreshAuthAsync()manually, orAuthRefreshOptions.POSTto{hub-or-connection-url}/refresh?id={connectionToken}with freshly acquired auth credentials.OnAuthRefreshcallback can accept or reject the refreshed principal.Server changes
/refreshendpoint alongside the existing negotiate/connect/send/poll endpoints.tokenLifetimeSecondswhen auth refresh is enabled and the auth ticket hasExpiresUtc./refreshrejects negotiate v0 connections because v0 has no private connection token (ConnectionId == ConnectionToken).CloseOnAuthenticationExpirationand auth refresh are both enabled.Endpoint metadata / auth behavior
OnAuthRefreshruns after the request has authenticated but before the connection user is replaced.OnAuthRefreshreturnsfalse, the connection keeps its current principal.OnAuthRefreshthrows, the exception is allowed to propagate through normal ASP.NET Core server error handling.Client changes
AuthRefreshOptions.HubConnection.RefreshAuthAsync()delegates to the underlying transport auth-refresh feature.RefreshAuthAsync()fetches a fresh access token rather than reusing the access token cached when the connection started.tokenLifetimeSecondsfrom refresh responses and uses it to schedule subsequent automatic refreshes.Refresh URL behavior
/refreshintentionally posts to the original configured client URL, not a negotiated redirect URL.idquery parameter.Hub-layer changes
Hub.OnAuthRefreshedAsync().UserIdentifierchanges, the connection is aborted.Hub.OnAuthRefreshedAsync()is invoked after the connection user state has been applied.Hub.OnAuthRefreshedAsync()is serialized throughActiveInvocationLimit, so it interleaves with hub invocations according toMaximumParallelInvocations.Hub caller context snapshots
DefaultHubCallerContextnow capturesUseras a snapshot.DefaultHubDispatchercaptures oneHubCallerContextper hub operation and uses that same snapshot for:HubInvocationContext,hub.Context.WindowsIdentity handling