diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index 532ec88b4..4d090b78c 100644 --- a/src/gateway/server.auth.default-token.suite.ts +++ b/src/gateway/server.auth.default-token.suite.ts @@ -157,10 +157,11 @@ export function registerDefaultAuthTokenSuite(): void { expectStatusError?: string; }> = [ { - name: "operator + valid shared token => connected with preserved scopes", + name: "operator + valid shared token => connected with cleared scopes", opts: { role: "operator", token, device: null }, expectConnectOk: true, - expectStatusOk: true, + expectStatusOk: false, + expectStatusError: "missing scope", }, { name: "node + valid shared token => rejected without device", diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index d327cd683..e226ebfc9 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -526,7 +526,10 @@ export function attachGatewayWsMessageHandler(params: { hasSharedAuth, isLocalClient, }); - if (!device && decision.kind !== "allow") { + // Shared token/password auth can bypass pairing for trusted operators, but + // device-less backend clients must not self-declare scopes. Control UI + // keeps its explicitly allowed device-less scopes on the allow path. + if (!device && (!isControlUi || decision.kind !== "allow")) { clearUnboundScopes(); } if (decision.kind === "allow") {