Long-lived SignalR connections create an interesting authentication problem.
A normal HTTP request is short-lived. The client sends an access token, the server validates it, and the request finishes. When the token expires, the next request can obtain a new token and continue normally.
A SignalR connection is different. A WebSocket connection, for example, can remain open for minutes or hours. If the access token used to establish that connection expires, the application needs a strategy for handling the authentication lifetime without unnecessarily disconnecting the user.
.NET 11 introduces authentication refresh for SignalR. A client can refresh its credentials on an existing connection without closing and reconnecting the connection.
This is particularly useful for applications such as real-time dashboards, collaboration tools, notifications, trading interfaces, support systems, multiplayer applications, and enterprise applications where reconnecting can interrupt an active workflow.
The Problem With Expiring Authentication
Consider a typical SignalR connection:
Client
|
| Access Token
v
SignalR Hub
|
| Authentication
v
Connection Established
|
| Real-time communication
|
| Token approaches expiration
v
Token Expires
Traditionally, an established SignalR connection keeps the authenticated principal associated with that connection.
That means a user's roles or claims can remain unchanged from the perspective of the existing connection even if the underlying authentication credentials later change.
For example, imagine a user initially connects with:
User: alice
Role: Editor
Tenant: Contoso
Later, the identity system issues a new token:
User: alice
Role: Admin
Tenant: Contoso
Without authentication refresh, the active SignalR connection does not automatically replace its cached authenticated principal.
The connection continues using the authentication information established when the connection was created.
What Changes in .NET 11?
.NET 11 introduces SignalR authentication refresh.
The client can send refreshed credentials to a dedicated refresh endpoint associated with the existing SignalR connection.
The server validates the refreshed authentication information and, when the new principal represents the same SignalR user, replaces the connection's cached ClaimsPrincipal.
The important distinction is:
Traditional approach:
Token expires
|
v
Connection closes
|
v
Client reconnects
|
v
Authentication happens again
With authentication refresh:
Token approaches expiration
|
v
Client obtains new token
|
v
Refresh existing connection
|
v
Server validates new credentials
|
v
Context.User is updated
|
v
Connection remains active
No normal disconnect/reconnect cycle is required.
Enabling Authentication Refresh
Authentication refresh is configured when mapping the SignalR hub.
A basic configuration looks like this:
app.MapHub<ChatHub>("/chat", options =>
{
options.EnableAuthenticationRefresh = true;
});
For applications where an expired token should eventually terminate the connection, it is useful to enable authentication expiration handling as well:
app.MapHub<ChatHub>("/chat", options =>
{
options.EnableAuthenticationRefresh = true;
options.CloseOnAuthenticationExpiration = true;
});
These options serve different purposes.
EnableAuthenticationRefresh allows the client to update its authentication credentials while keeping the existing connection.
CloseOnAuthenticationExpiration ensures that a connection isn't allowed to remain open indefinitely after its authentication expires.
Together, they provide a predictable lifecycle:
Valid Token
|
v
Active Connection
|
v
Refresh Before Expiration
|
+---- Success ----> Continue Connection
|
+---- Failure ----> Token Expires
|
v
Connection Closes
This is generally preferable to allowing an old authentication state to remain active indefinitely.
Configuring JWT Authentication
Authentication refresh is particularly useful with bearer-token authentication.
A simplified ASP.NET Core configuration can look like this:
builder.Services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://identity.example";
options.Audience = "realtime-api";
});
builder.Services.AddAuthorization();
builder.Services.AddSignalR();
The middleware pipeline must also include authentication and authorization:
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<ChatHub>("/chat", options =>
{
options.EnableAuthenticationRefresh = true;
options.CloseOnAuthenticationExpiration = true;
});
The exact JWT configuration depends on the identity provider used by the application.
The important part is that the SignalR endpoint uses the same authentication infrastructure that validates the refreshed credentials.
How the .NET SignalR Client Refreshes Authentication
The .NET SignalR client uses AccessTokenProvider to obtain the current access token.
For example:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chat", options =>
{
options.AccessTokenProvider = GetAccessTokenAsync;
})
.Build();
await connection.StartAsync();
The token provider should return a current access token:
private async Task<string?> GetAccessTokenAsync()
{
return await tokenService.GetAccessTokenAsync();
}
When authentication needs to be refreshed, SignalR calls the provider and uses the newly returned token.
This is important because the client should not simply return the token that was originally used to establish the connection.
Explicitly Refreshing Authentication
.NET 11 provides RefreshAuthenticationAsync for explicitly refreshing authentication.
For example:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chat", options =>
{
options.AccessTokenProvider = GetAccessTokenAsync;
})
.Build();
await connection.StartAsync();
TimeSpan? lifetime =
await connection.RefreshAuthenticationAsync();
The returned value represents the new token lifetime reported by the server.
This makes it possible to build application-specific refresh logic when the default automatic behavior isn't appropriate.
Automatic Authentication Refresh
For most applications, automatic refresh is more convenient.
The .NET SignalR client provides WithAuthenticationRefresh:
var connection = new HubConnectionBuilder()
.WithUrl("https://example.com/chat", options =>
{
options.AccessTokenProvider = GetAccessTokenAsync;
})
.WithAuthenticationRefresh(options =>
{
options.RefreshBeforeExpiration =
TimeSpan.FromMinutes(2);
})
.Build();
The client can then refresh the authentication before the reported expiration time.
The refresh window can be adjusted according to the application's requirements.
For example:
options.RefreshBeforeExpiration =
TimeSpan.FromMinutes(5);
A longer window can provide additional protection against temporary delays, network latency, or token acquisition problems.
However, refreshing too early can also increase authentication traffic unnecessarily.
Handling Refresh Events
Applications may need to know whether authentication was successfully refreshed.
The .NET client exposes authentication refresh events.
For example:
connection.AuthenticationRefreshed += context =>
{
Console.WriteLine(
$"Authentication refreshed. " +
$"New lifetime: {context.NewTokenLifetime}");
return Task.CompletedTask;
};
A failure handler can also be registered:
connection.AuthenticationRefreshFailed += context =>
{
Console.WriteLine(
$"Authentication refresh failed: " +
$"{context.Exception}");
return Task.CompletedTask;
};
These events are useful for diagnostics and operational monitoring.
For example, an application could record:
Refresh success.
Refresh failure.
Authentication expiration.
Connection termination.
Token acquisition failures.
Avoid logging the actual access token.
Refreshing Claims and Roles
One of the most useful aspects of authentication refresh is updating claims and roles on an existing connection.
Suppose a user initially connects as:
User: john
Role: User
An administrator later grants the user an additional role:
User: john
Role: Manager
A refreshed token can contain the new role.
After a successful authentication refresh, subsequent hub invocations can see the refreshed principal through Context.User.
For example:
public class ChatHub : Hub
{
public Task<string> GetCurrentRole()
{
var role = Context.User?
.FindFirst("role")?
.Value;
return Task.FromResult(role ?? "Unknown");
}
}
After the refresh succeeds, later invocations can use the updated claims.
This provides a much cleaner model than forcing the client to reconnect solely because a user's authorization information changed.
The User Identity Cannot Change
There is an important security restriction.
A refreshed authentication principal must represent the same SignalR user as the existing connection.
For example:
Existing connection:
User ID = 123
Refreshed token:
User ID = 123
This is valid.
But:
Existing connection:
User ID = 123
Refreshed token:
User ID = 456
Join the conversation! Your thoughts help the community grow.