Skip to content

Add SignalR Auth Refresh support to server and .NET client - #67111

Merged
BrennanConroy merged 22 commits into
mainfrom
brecon/authrefresh
Jun 24, 2026
Merged

Add SignalR Auth Refresh support to server and .NET client#67111
BrennanConroy merged 22 commits into
mainfrom
brecon/authrefresh

Conversation

@BrennanConroy

@BrennanConroy BrennanConroy commented Jun 10, 2026

Copy link
Copy Markdown
Member

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

  1. The server enables auth refresh with HttpConnectionDispatcherOptions.EnableAuthRefresh.
  2. If the current auth ticket has an expiration, negotiate includes tokenLifetimeSeconds.
  3. The .NET client exposes that initial lifetime and can either:
    • call HubConnection.RefreshAuthAsync() manually, or
    • configure automatic refresh through AuthRefreshOptions.
  4. The client sends a POST to {hub-or-connection-url}/refresh?id={connectionToken} with freshly acquired auth credentials.
  5. The server validates the refresh request using the endpoint auth metadata.
  6. The optional OnAuthRefresh callback can accept or reject the refreshed principal.
  7. If accepted, the connection principal and auth expiration are updated.
  8. Existing in-flight hub methods keep the caller context they started with so they don't see a different User over the lifetime of the individual hub method call.

Server changes

  • Adds an internal /refresh endpoint alongside the existing negotiate/connect/send/poll endpoints.
  • Negotiation can include tokenLifetimeSeconds when auth refresh is enabled and the auth ticket has ExpiresUtc.
  • /refresh rejects negotiate v0 connections because v0 has no private connection token (ConnectionId == ConnectionToken).
  • Expired connections get an auth-refresh grace period before connection cleanup when CloseOnAuthenticationExpiration and auth refresh are both enabled.

Endpoint metadata / auth behavior

  • The refresh endpoint is stamped with the same authorization metadata and endpoint conventions as the connection endpoint.
  • OnAuthRefresh runs after the request has authenticated but before the connection user is replaced.
  • If OnAuthRefresh returns false, the connection keeps its current principal.
  • If OnAuthRefresh throws, the exception is allowed to propagate through normal ASP.NET Core server error handling.

Client changes

  • Adds client auth-refresh options through 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.
  • The refreshed token is also cached for subsequent transport requests, so later Long Polling polls/sends use the refreshed credential.
  • The client parses tokenLifetimeSeconds from refresh responses and uses it to schedule subsequent automatic refreshes.
  • Automatic refresh is re-armed after reconnect.

Refresh URL behavior

  • /refresh intentionally posts to the original configured client URL, not a negotiated redirect URL.
  • This treats refresh as part of the application authentication plane.
  • The request uses the private connection token as the id query parameter.

Hub-layer changes

  • Adds overridable Hub.OnAuthRefreshedAsync().
  • The hub layer subscribes to the connection user-update feature.
  • If the UserIdentifier changes, the connection is aborted.
  • Hub.OnAuthRefreshedAsync() is invoked after the connection user state has been applied.
  • Hub.OnAuthRefreshedAsync() is serialized through ActiveInvocationLimit, so it interleaves with hub invocations according to MaximumParallelInvocations.

Hub caller context snapshots

  • DefaultHubCallerContext now captures User as a snapshot.
  • DefaultHubDispatcher captures one HubCallerContext per hub operation and uses that same snapshot for:
    • authorization,
    • hub filters,
    • HubInvocationContext,
    • and hub.Context.
  • In-flight hub methods continue to see the old caller context after auth refresh.
  • Hub methods that start after the refresh see the new caller context.

WindowsIdentity handling

  • WindowsIdentity refreshes are rejected. By default there isn't an expiration claim so automatic refreshes shouldn't occur anyways. And handling the SafeHandle across requests adds a lot of extra complexity. Can be reconsidered later.

@BrennanConroy BrennanConroy added the area-signalr Includes: SignalR clients and servers label Jun 10, 2026
BrennanConroy and others added 13 commits June 17, 2026 10:46
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>
BrennanConroy and others added 2 commits June 18, 2026 15:18
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>
BrennanConroy and others added 2 commits June 22, 2026 11:39
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>
BrennanConroy and others added 2 commits June 22, 2026 13:40
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>
@BrennanConroy
BrennanConroy marked this pull request as ready for review June 22, 2026 22:03
@BrennanConroy
BrennanConroy requested a review from halter73 as a code owner June 22, 2026 22:03
Copilot AI review requested due to automatic review settings June 22, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /refresh endpoint + EnableAuthenticationRefresh/OnAuthenticationRefresh options, and negotiate metadata (tokenLifetimeSeconds) for token lifetime.
  • Hub layer: apply refreshed principals safely (snapshot HubCallerContext.User, reject UserIdentifier changes by aborting), and add Hub.OnAuthenticationRefreshedAsync().
  • .NET client: add HubConnection.RefreshAuthenticationAsync() and auto-refresh scheduling via AuthenticationRefreshOptions, 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

Comment thread src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs
Comment thread src/SignalR/server/Core/src/HubConnectionHandler.cs
Comment thread src/SignalR/samples/JwtClientSample/Program.cs
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@halter73

Copy link
Copy Markdown
Member

API Proposal for (back) reference: #67226

@halter73 halter73 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

@halter73 halter73 Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@BrennanConroy
BrennanConroy merged commit 8835dfc into main Jun 24, 2026
25 checks passed
@BrennanConroy
BrennanConroy deleted the brecon/authrefresh branch June 24, 2026 07:23
@BrennanConroy

Copy link
Copy Markdown
Member Author

/backport to release/11.0-preview6

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0-preview6 (link to workflow run)

@github-actions

Copy link
Copy Markdown
Contributor

@BrennanConroy backporting to release/11.0-preview6 failed, the patch most likely resulted in conflicts. Please backport manually!

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

Link to workflow output

wtgodbe pushed a commit that referenced this pull request Jun 24, 2026
…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)
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-signalr Includes: SignalR clients and servers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants