Skip to content

Commit e8156e8

Browse files
Add warning if user changes during Stateful Reconnect (#50059)
1 parent 8bfb367 commit e8156e8

File tree

4 files changed

+96
-5
lines changed

4 files changed

+96
-5
lines changed

src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2666,6 +2666,78 @@ public async Task CanReconnectAndSendMessageOnceConnected()
26662666
}
26672667
}
26682668

2669+
[Fact]
2670+
public async Task ChangingUserNameDuringReconnectLogsWarning()
2671+
{
2672+
var protocol = HubProtocols["json"];
2673+
await using (var server = await StartServer<Startup>())
2674+
{
2675+
var websocket = new ClientWebSocket();
2676+
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
2677+
2678+
var userName = "test1";
2679+
var connectionBuilder = new HubConnectionBuilder()
2680+
.WithLoggerFactory(LoggerFactory)
2681+
.WithUrl(server.Url + "/default", HttpTransportType.WebSockets, o =>
2682+
{
2683+
o.WebSocketFactory = async (context, token) =>
2684+
{
2685+
var httpResponse = await new HttpClient().GetAsync(server.Url + $"/generateJwtToken/{userName}");
2686+
httpResponse.EnsureSuccessStatusCode();
2687+
var authHeader = await httpResponse.Content.ReadAsStringAsync();
2688+
websocket.Options.SetRequestHeader("Authorization", $"Bearer {authHeader}");
2689+
2690+
await websocket.ConnectAsync(context.Uri, token);
2691+
tcs.SetResult();
2692+
return websocket;
2693+
};
2694+
o.UseAcks = true;
2695+
})
2696+
.WithAutomaticReconnect();
2697+
connectionBuilder.Services.AddSingleton(protocol);
2698+
var connection = connectionBuilder.Build();
2699+
2700+
var reconnectCalled = false;
2701+
connection.Reconnecting += ex =>
2702+
{
2703+
reconnectCalled = true;
2704+
return Task.CompletedTask;
2705+
};
2706+
2707+
try
2708+
{
2709+
await connection.StartAsync().DefaultTimeout();
2710+
userName = "test2";
2711+
await tcs.Task;
2712+
tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
2713+
2714+
var originalConnectionId = connection.ConnectionId;
2715+
2716+
var originalWebsocket = websocket;
2717+
websocket = new ClientWebSocket();
2718+
2719+
originalWebsocket.Dispose();
2720+
2721+
await tcs.Task.DefaultTimeout();
2722+
2723+
Assert.Equal(originalConnectionId, connection.ConnectionId);
2724+
Assert.False(reconnectCalled);
2725+
2726+
var changeLog = Assert.Single(TestSink.Writes.Where(w => w.EventId.Name == "UserNameChanged"));
2727+
Assert.EndsWith("The name of the user changed from 'test1' to 'test2'.", changeLog.Message);
2728+
}
2729+
catch (Exception ex)
2730+
{
2731+
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
2732+
throw;
2733+
}
2734+
finally
2735+
{
2736+
await connection.DisposeAsync().DefaultTimeout();
2737+
}
2738+
}
2739+
}
2740+
26692741
[Fact]
26702742
public async Task ServerAbortsConnectionWithAckingEnabledNoReconnectAttempted()
26712743
{

src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public void ConfigureServices(IServiceCollection services)
4747
});
4848
});
4949

50+
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
5051
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
5152
.AddJwtBearer(options =>
5253
{
@@ -60,7 +61,6 @@ public void ConfigureServices(IServiceCollection services)
6061
IssuerSigningKey = SecurityKey
6162
};
6263
});
63-
services.AddAuthentication(NegotiateDefaults.AuthenticationScheme).AddNegotiate();
6464

6565
// Since tests run in parallel, it's possible multiple servers will startup,
6666
// we use an ephemeral key provider and repository to avoid filesystem contention issues
@@ -114,9 +114,9 @@ public void Configure(IApplicationBuilder app)
114114
options.MinimumProtocolVersion = -1;
115115
});
116116

117-
endpoints.MapGet("/generateJwtToken", context =>
117+
endpoints.MapGet("/generateJwtToken/{name?}", (HttpContext context, string name) =>
118118
{
119-
return context.Response.WriteAsync(GenerateJwtToken());
119+
return context.Response.WriteAsync(GenerateJwtToken(name ?? "testuser"));
120120
});
121121

122122
endpoints.Map("/redirect/{*anything}", context =>
@@ -130,9 +130,9 @@ public void Configure(IApplicationBuilder app)
130130
});
131131
}
132132

133-
private string GenerateJwtToken()
133+
private string GenerateJwtToken(string name = "testuser")
134134
{
135-
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "testuser") };
135+
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, name) };
136136
var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
137137
var token = new JwtSecurityToken("SignalRTestServer", "SignalRTests", claims, expires: DateTime.Now.AddSeconds(5), signingCredentials: credentials);
138138
return JwtTokenHandler.WriteToken(token);

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.Log.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,13 @@ internal static partial class Log
5656

5757
[LoggerMessage(16, LogLevel.Debug, "The client requested an invalid protocol version '{queryStringVersionValue}'", EventName = "InvalidNegotiateProtocolVersion")]
5858
public static partial void InvalidNegotiateProtocolVersion(ILogger logger, string queryStringVersionValue);
59+
60+
[LoggerMessage(17, LogLevel.Warning, "The name of the user changed from '{PreviousUserName}' to '{CurrentUserName}'.", EventName = "UserNameChanged")]
61+
private static partial void UserNameChangedInternal(ILogger logger, string previousUserName, string currentUserName);
62+
63+
public static void UserNameChanged(ILogger logger, string? previousUserName, string? currentUserName)
64+
{
65+
UserNameChangedInternal(logger, previousUserName ?? "(null)", currentUserName ?? "(null)");
66+
}
5967
}
6068
}

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionDispatcher.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,17 @@ private async Task<bool> EnsureConnectionStateAsync(HttpConnectionContext connec
606606
connection.HttpContext = context;
607607
}
608608

609+
if (connection.User is not null)
610+
{
611+
var originalName = connection.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
612+
var newName = connection.HttpContext?.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
613+
if (originalName != newName)
614+
{
615+
// Log warning, different user
616+
Log.UserNameChanged(_logger, originalName, newName);
617+
}
618+
}
619+
609620
// Setup the connection state from the http context
610621
connection.User = connection.HttpContext?.User;
611622

0 commit comments

Comments
 (0)