fix(cli): no redirects on the management client, race-safe token refresh, verbose Basic-fallback note (review 2026-10-21 05/U1, 05/P1 L1/L2/L3/L4)

- CreateDefaultHttpClient sets AllowAutoRedirect=false so a 307/308 cannot re-POST the
  /auth/token credential form elsewhere.
- A forced re-exchange after a 401 carries the refused token; inside the gate it reuses
  a token another caller already re-exchanged instead of binding again. The BasicOnly
  switch is written under the same gate.
- SCADABRIDGE_VERBOSE=1 prints a stderr note when a fresh token is refused and the CLI
  falls back to Basic.
- debug stream's AccessTokenProvider retries the exchange once and documents the
  null-token degradation (the hub refuses, automatic reconnect retries).
- Tests: concurrent refusal reuses the peer refresh (2 exchanges, not 3); real-socket
  redirect is not followed.
This commit is contained in:
Joseph Doherty
2026-10-22 08:25:21 -04:00
parent ea172edebc
commit e968566e9d
5 changed files with 157 additions and 11 deletions
@@ -120,8 +120,13 @@ public static class DebugCommands
{
if (bearer.Token is not null)
{
// Re-invoked on every (re)connect. A null token means the exchange fell back
// (transient error, or the server dropped bearer support mid-stream): retry
// once, and if it is still null connect without a token — the hub refuses the
// handshake and WithAutomaticReconnect tries again on its backoff schedule.
options.AccessTokenProvider = async () =>
(await authClient.TryGetBearerTokenAsync(CancellationToken.None)).Token;
(await authClient.TryGetBearerTokenAsync(CancellationToken.None)).Token
?? (await authClient.TryGetBearerTokenAsync(CancellationToken.None)).Token;
}
else
{