fix: allow control ui token auth without pairing
This commit is contained in:
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import { WebSocket } from "ws";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
import { getHandshakeTimeoutMs } from "./server-constants.js";
|
||||
import { buildDeviceAuthPayload } from "./device-auth.js";
|
||||
import {
|
||||
connectReq,
|
||||
getFreePort,
|
||||
@@ -286,6 +287,70 @@ describe("gateway server auth/connect", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("allows control ui with device identity when insecure auth is enabled", async () => {
|
||||
testState.gatewayControlUi = { allowInsecureAuth: true };
|
||||
const { writeConfigFile } = await import("../config/config.js");
|
||||
await writeConfigFile({
|
||||
gateway: {
|
||||
trustedProxies: ["127.0.0.1"],
|
||||
},
|
||||
} as any);
|
||||
const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = "secret";
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port);
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}`, {
|
||||
headers: { "x-forwarded-for": "203.0.113.10" },
|
||||
});
|
||||
const challengePromise = onceMessage<{ payload?: unknown }>(
|
||||
ws,
|
||||
(o) => o.type === "event" && o.event === "connect.challenge",
|
||||
);
|
||||
await new Promise<void>((resolve) => ws.once("open", resolve));
|
||||
const challenge = await challengePromise;
|
||||
const nonce = (challenge.payload as { nonce?: unknown } | undefined)?.nonce;
|
||||
expect(typeof nonce).toBe("string");
|
||||
const { loadOrCreateDeviceIdentity, publicKeyRawBase64UrlFromPem, signDevicePayload } =
|
||||
await import("../infra/device-identity.js");
|
||||
const identity = loadOrCreateDeviceIdentity();
|
||||
const signedAtMs = Date.now();
|
||||
const payload = buildDeviceAuthPayload({
|
||||
deviceId: identity.deviceId,
|
||||
clientId: GATEWAY_CLIENT_NAMES.CONTROL_UI,
|
||||
clientMode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||
role: "operator",
|
||||
scopes: [],
|
||||
signedAtMs,
|
||||
token: "secret",
|
||||
nonce: String(nonce),
|
||||
});
|
||||
const device = {
|
||||
id: identity.deviceId,
|
||||
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
||||
signature: signDevicePayload(identity.privateKeyPem, payload),
|
||||
signedAt: signedAtMs,
|
||||
nonce: String(nonce),
|
||||
};
|
||||
const res = await connectReq(ws, {
|
||||
token: "secret",
|
||||
device,
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.CONTROL_UI,
|
||||
version: "1.0.0",
|
||||
platform: "web",
|
||||
mode: GATEWAY_CLIENT_MODES.WEBCHAT,
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
ws.close();
|
||||
await server.close();
|
||||
if (prevToken === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken;
|
||||
}
|
||||
});
|
||||
|
||||
test("accepts device token auth for paired device", async () => {
|
||||
const { loadOrCreateDeviceIdentity } = await import("../infra/device-identity.js");
|
||||
const { approveDevicePairing, getPairedDevice, listDevicePairing } =
|
||||
|
||||
@@ -318,13 +318,13 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
let devicePublicKey: string | null = null;
|
||||
const hasTokenAuth = Boolean(connectParams.auth?.token);
|
||||
const hasPasswordAuth = Boolean(connectParams.auth?.password);
|
||||
const hasSharedAuth = hasTokenAuth || hasPasswordAuth;
|
||||
const isControlUi = connectParams.client.id === GATEWAY_CLIENT_IDS.CONTROL_UI;
|
||||
const allowInsecureControlUi =
|
||||
isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
|
||||
|
||||
if (!device) {
|
||||
const allowInsecureControlUi =
|
||||
isControlUi && configSnapshot.gateway?.controlUi?.allowInsecureAuth === true;
|
||||
const canSkipDevice =
|
||||
isControlUi && allowInsecureControlUi ? hasTokenAuth || hasPasswordAuth : hasTokenAuth;
|
||||
const canSkipDevice = allowInsecureControlUi ? hasSharedAuth : hasTokenAuth;
|
||||
|
||||
if (isControlUi && !allowInsecureControlUi) {
|
||||
const errorMessage = "control ui requires HTTPS or localhost (secure context)";
|
||||
@@ -569,7 +569,8 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (device && devicePublicKey) {
|
||||
const skipPairing = allowInsecureControlUi && hasSharedAuth;
|
||||
if (device && devicePublicKey && !skipPairing) {
|
||||
const requirePairing = async (reason: string, _paired?: { deviceId: string }) => {
|
||||
const pairing = await requestDevicePairing({
|
||||
deviceId: device.id,
|
||||
|
||||
Reference in New Issue
Block a user