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

is rejected.

This prevents authentication refresh from becoming a mechanism for transferring an existing connection from one user identity to another.

The connection's Context.UserIdentifier and SignalR user routing identity remain associated with the original connection identity.

If the application genuinely needs to change the connected user, the client should establish a new connection.

Validating Refreshes on the Server

Applications can perform additional checks using OnAuthenticationRefresh.

For example:

app.MapHub<ChatHub>("/chat", options =>
{
    options.EnableAuthenticationRefresh = true;

    options.OnAuthenticationRefresh = context =>
    {
        if (!context.NewUser.HasClaim(
                "tenant",
                "contoso"))
        {
            return ValueTask.FromResult(false);
        }

        return ValueTask.FromResult(true);
    };
});

This provides an additional application-level validation step.

The callback receives information about the newly authenticated principal.

An application can use this to enforce rules such as:

  • Tenant membership.

  • Required claims.

  • Account state.

  • Security policies.

  • Organization membership.

  • Application-specific restrictions.

Returning false rejects the refresh.

The existing connection remains connected with its previous authentication information.

Limiting Authentication Lifetime

Applications should also consider how far authentication refresh can extend a connection.

.NET 11 provides MaximumAuthenticationExpiration:

app.MapHub<ChatHub>("/chat", options =>
{
    options.EnableAuthenticationRefresh = true;
    options.CloseOnAuthenticationExpiration = true;

    options.MaximumAuthenticationExpiration =
        TimeSpan.FromHours(8);
});

This limits how far into the future a refreshed authentication expiration can be extended.

This is useful for applications that want long-lived connections but still require periodic authentication renewal.

For example:

Connection
    |
    +--> Refresh
    |
    +--> Refresh
    |
    +--> Refresh
    |
    +--> Maximum Lifetime Reached
    |
    v
Reconnect / Reauthenticate

This gives security teams a predictable maximum authentication lifetime.

What Happens If Refresh Fails?

Authentication refresh should be treated as a normal failure scenario.

Possible causes include:

  • Refresh token has expired.

  • Identity provider is unavailable.

  • User has been disabled.

  • Required claims are missing.

  • Tenant membership has changed.

  • Token signature is invalid.

  • Token audience is incorrect.

  • Additional server validation rejects the refresh.

A robust client should handle these cases explicitly.

For example:

connection.AuthenticationRefreshFailed += async context =>
{
    logger.LogWarning(
        context.Exception,
        "SignalR authentication refresh failed.");

    await HandleAuthenticationFailureAsync();
};

Do not blindly retry authentication failures forever.

If the identity system rejects the user, repeated refresh attempts can create unnecessary traffic and make troubleshooting more difficult.

Authentication Refresh and Active Hub Invocations

Authentication refresh does not retroactively change a hub method that is already executing.

Consider:

Hub Method A starts
       |
       v
Authentication Refresh
       |
       v
Hub Method A continues

The already-running invocation keeps the authentication context it started with.

Later hub invocations see the refreshed principal.

This distinction is important when designing authorization-sensitive operations.

If an operation is long-running and performs multiple security-sensitive actions, consider whether authorization should be evaluated again against current application state rather than assuming that the principal remains unchanged for the entire operation.

Authentication Refresh vs Reconnection

These two mechanisms solve different problems.

Feature

Authentication Refresh

Reconnection

Existing connection

Preserved

Replaced

Connection ID

Preserved

New connection

Authentication

Refreshed

Re-established

Missed messages

No intentional connection gap

May occur

Group membership

Preserved

Usually must be restored

User routing

Preserved

Re-established

Best use

Credential renewal

Connection failure

Network failure

Doesn't solve it

Designed for it

Authentication refresh should therefore not replace SignalR reconnection logic.

A production application generally needs both.

Security Considerations

Authentication refresh improves connection continuity, but it does not eliminate security responsibilities.

Use HTTPS

Always protect SignalR communication with HTTPS.

Bearer credentials can be transmitted differently depending on the transport and client environment, so encrypted transport is essential.

Do Not Log Access Tokens

Avoid code such as:

logger.LogInformation(
    "SignalR token: {Token}",
    accessToken);

Tokens should never be unnecessarily written to logs.

Validate the Refreshed Principal

Do not treat every refreshed token as automatically trusted beyond normal authentication validation.

Use OnAuthenticationRefresh when additional application-level checks are required.

Keep Token Lifetimes Reasonable

Authentication refresh should not become a reason to issue effectively permanent credentials.

Use MaximumAuthenticationExpiration when the application requires a hard limit.

Keep Authorization Checks Current

Authentication refresh updates the SignalR connection's principal, but application data can change independently.

For example, a user's database permissions may be revoked without changing the identity provider token.

For especially sensitive operations, application-level authorization checks can still be appropriate.

A Complete Example

A simplified server configuration might look like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication("Bearer")
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://identity.example";
        options.Audience = "chat-api";
    });

builder.Services.AddAuthorization();
builder.Services.AddSignalR();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapHub<ChatHub>("/chat", options =>
{
    options.EnableAuthenticationRefresh = true;
    options.CloseOnAuthenticationExpiration = true;

    options.MaximumAuthenticationExpiration =
        TimeSpan.FromHours(8);

    options.OnAuthenticationRefresh = context =>
    {
        if (!context.NewUser.HasClaim(
                "tenant",
                "contoso"))
        {
            return ValueTask.FromResult(false);
        }

        return ValueTask.FromResult(true);
    };
});

app.Run();

The corresponding .NET client can use:

var connection = new HubConnectionBuilder()
    .WithUrl("https://example.com/chat", options =>
    {
        options.AccessTokenProvider =
            GetAccessTokenAsync;
    })
    .WithAuthenticationRefresh(options =>
    {
        options.RefreshBeforeExpiration =
            TimeSpan.FromMinutes(5);
    })
    .Build();

connection.AuthenticationRefreshed += context =>
{
    logger.LogInformation(
        "SignalR authentication refreshed.");

    return Task.CompletedTask;
};

connection.AuthenticationRefreshFailed += context =>
{
    logger.LogWarning(
        context.Exception,
        "SignalR authentication refresh failed.");

    return Task.CompletedTask;
};

await connection.StartAsync();

This pattern gives the application:

  1. An authenticated SignalR connection.

  2. Automatic token refresh before expiration.

  3. Server-side validation of the refreshed principal.

  4. A maximum authentication lifetime.

  5. A fallback when refresh fails.

  6. No intentional disconnect during successful authentication renewal.

Common Mistakes

Refreshing the Token but Not the SignalR Connection

Obtaining a new access token in the application's authentication service does not by itself update the SignalR connection's cached principal.

The SignalR authentication refresh mechanism must be invoked.

Returning the Old Token

If AccessTokenProvider always returns the token originally acquired during login, refreshing the connection cannot provide new credentials.

The provider must obtain the current token.

Assuming Refresh Changes the User

A refresh can update claims and roles, but it cannot change the SignalR connection to another user identity.

Disabling Expiration Handling

Enabling refresh without considering what happens when refresh repeatedly fails can leave the application's connection lifecycle unclear.

For security-sensitive applications, combine authentication refresh with an explicit expiration policy.

Treating Refresh as Reconnection

Authentication refresh is not a replacement for handling network interruptions.

A production SignalR client should still have appropriate connection retry and recovery logic.

Best Practices

For production SignalR applications using .NET 11 authentication refresh:

  1. Enable EnableAuthenticationRefresh only on hubs that require it.

  2. Use CloseOnAuthenticationExpiration when stale authentication should terminate the connection.

  3. Keep AccessTokenProvider or the equivalent token provider capable of returning fresh credentials.

  4. Configure a sensible refresh-before-expiration window.

  5. Use MaximumAuthenticationExpiration when connections must have a hard authentication lifetime.

  6. Validate important application-specific claims with OnAuthenticationRefresh.

  7. Never log access tokens.

  8. Keep HTTPS enabled.

  9. Continue implementing normal SignalR reconnect handling.

  10. Test role and claim changes while a connection is active.

  11. Test refresh failures and expired credentials.

  12. Monitor refresh failures separately from network connection failures.

Conclusion

SignalR authentication refresh in .NET 11 addresses an important problem with long-lived real-time connections.

Previously, applications often had to choose between allowing an existing connection to continue with its original authentication state or closing the connection and forcing the client to reconnect when credentials needed to be renewed.

.NET 11 provides a third option.

The client can obtain a new token, send it through the SignalR authentication refresh mechanism, and allow the server to replace the connection's authenticated principal without dropping the active connection.

The feature is particularly useful when users remain connected for long periods and authentication claims can change during that time.

The key design principle is simple:

Long-lived connection
        +
Short-lived credentials
        =
Authentication refresh

Used together with expiration handling, server-side validation, sensible token lifetimes, and normal reconnection logic, authentication refresh provides a more reliable way to manage security and connection continuity in modern ASP.NET Core SignalR applications.