mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:01:23 +00:00
fix: require gateway auth by default
This commit is contained in:
@@ -173,8 +173,7 @@ export function resolveGatewayAuth(params: {
|
||||
const env = params.env ?? process.env;
|
||||
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
||||
const password = authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
||||
const mode: ResolvedGatewayAuth["mode"] =
|
||||
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
||||
const mode: ResolvedGatewayAuth["mode"] = authConfig.mode ?? (password ? "password" : "token");
|
||||
const allowTailscale =
|
||||
authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password");
|
||||
return {
|
||||
@@ -187,6 +186,7 @@ export function resolveGatewayAuth(params: {
|
||||
|
||||
export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
||||
if (auth.mode === "token" && !auth.token) {
|
||||
if (auth.allowTailscale) return;
|
||||
throw new Error(
|
||||
"gateway auth mode is token, but no token was configured (set gateway.auth.token or CLAWDBOT_GATEWAY_TOKEN)",
|
||||
);
|
||||
|
||||
@@ -70,6 +70,11 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
tailscaleMode,
|
||||
});
|
||||
const authMode: ResolvedGatewayAuth["mode"] = resolvedAuth.mode;
|
||||
const hasToken = typeof resolvedAuth.token === "string" && resolvedAuth.token.trim().length > 0;
|
||||
const hasPassword =
|
||||
typeof resolvedAuth.password === "string" && resolvedAuth.password.trim().length > 0;
|
||||
const hasSharedSecret =
|
||||
(authMode === "token" && hasToken) || (authMode === "password" && hasPassword);
|
||||
const hooksConfig = resolveHooksConfig(params.cfg);
|
||||
const canvasHostEnabled =
|
||||
process.env.CLAWDBOT_SKIP_CANVAS_HOST !== "1" && params.cfg.canvasHost?.enabled !== false;
|
||||
@@ -83,9 +88,9 @@ export async function resolveGatewayRuntimeConfig(params: {
|
||||
if (tailscaleMode !== "off" && !isLoopbackHost(bindHost)) {
|
||||
throw new Error("tailscale serve/funnel requires gateway bind=loopback (127.0.0.1)");
|
||||
}
|
||||
if (!isLoopbackHost(bindHost) && authMode === "none") {
|
||||
if (!isLoopbackHost(bindHost) && !hasSharedSecret) {
|
||||
throw new Error(
|
||||
`refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token or CLAWDBOT_GATEWAY_TOKEN, or pass --token)`,
|
||||
`refusing to bind gateway to ${bindHost}:${params.port} without auth (set gateway.auth.token/password, or set CLAWDBOT_GATEWAY_TOKEN/CLAWDBOT_GATEWAY_PASSWORD)`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ const openWs = async (port: number) => {
|
||||
};
|
||||
|
||||
describe("gateway server auth/connect", () => {
|
||||
describe("default auth", () => {
|
||||
describe("default auth (token)", () => {
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>>;
|
||||
let port: number;
|
||||
|
||||
@@ -234,6 +234,7 @@ describe("gateway server auth/connect", () => {
|
||||
test("returns control ui hint when token is missing", async () => {
|
||||
const ws = await openWs(port);
|
||||
const res = await connectReq(ws, {
|
||||
skipDefaultAuth: true,
|
||||
client: {
|
||||
id: GATEWAY_CLIENT_NAMES.CONTROL_UI,
|
||||
version: "1.0.0",
|
||||
@@ -352,6 +353,7 @@ describe("gateway server auth/connect", () => {
|
||||
});
|
||||
|
||||
test("rejects proxied connections without auth when proxy headers are untrusted", async () => {
|
||||
testState.gatewayAuth = { mode: "none" };
|
||||
const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const port = await getFreePort();
|
||||
@@ -360,7 +362,7 @@ describe("gateway server auth/connect", () => {
|
||||
headers: { "x-forwarded-for": "203.0.113.10" },
|
||||
});
|
||||
await new Promise<void>((resolve) => ws.once("open", resolve));
|
||||
const res = await connectReq(ws);
|
||||
const res = await connectReq(ws, { skipDefaultAuth: true });
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.error?.message ?? "").toContain("gateway auth required");
|
||||
ws.close();
|
||||
|
||||
@@ -28,11 +28,12 @@ let ws: WebSocket;
|
||||
let port: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const started = await startServerWithClient();
|
||||
const token = "test-gateway-token-1234567890";
|
||||
const started = await startServerWithClient(token);
|
||||
server = started.server;
|
||||
ws = started.ws;
|
||||
port = started.port;
|
||||
await connectOk(ws);
|
||||
await connectOk(ws, { token });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -60,6 +61,7 @@ describe("late-arriving invoke results", () => {
|
||||
mode: GATEWAY_CLIENT_MODES.NODE,
|
||||
},
|
||||
commands: ["canvas.snapshot"],
|
||||
token: "test-gateway-token-1234567890",
|
||||
});
|
||||
|
||||
// Send an invoke result with an unknown ID (simulating late arrival after timeout)
|
||||
|
||||
@@ -111,7 +111,7 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) {
|
||||
sessionStoreSaveDelayMs.value = 0;
|
||||
testTailnetIPv4.value = undefined;
|
||||
testState.gatewayBind = undefined;
|
||||
testState.gatewayAuth = undefined;
|
||||
testState.gatewayAuth = { mode: "token", token: "test-gateway-token-1234567890" };
|
||||
testState.gatewayControlUi = undefined;
|
||||
testState.hooksConfig = undefined;
|
||||
testState.canvasHostPort = undefined;
|
||||
@@ -260,10 +260,15 @@ export async function startGatewayServer(port: number, opts?: GatewayServerOptio
|
||||
export async function startServerWithClient(token?: string, opts?: GatewayServerOptions) {
|
||||
let port = await getFreePort();
|
||||
const prev = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
if (token === undefined) {
|
||||
const fallbackToken =
|
||||
token ??
|
||||
(typeof (testState.gatewayAuth as { token?: unknown } | undefined)?.token === "string"
|
||||
? (testState.gatewayAuth as { token?: string }).token
|
||||
: undefined);
|
||||
if (fallbackToken === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = token;
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = fallbackToken;
|
||||
}
|
||||
|
||||
let server: Awaited<ReturnType<typeof startGatewayServer>> | null = null;
|
||||
@@ -299,6 +304,7 @@ export async function connectReq(
|
||||
opts?: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
skipDefaultAuth?: boolean;
|
||||
minProtocol?: number;
|
||||
maxProtocol?: number;
|
||||
client?: {
|
||||
@@ -334,6 +340,20 @@ export async function connectReq(
|
||||
mode: GATEWAY_CLIENT_MODES.TEST,
|
||||
};
|
||||
const role = opts?.role ?? "operator";
|
||||
const defaultToken =
|
||||
opts?.skipDefaultAuth === true
|
||||
? undefined
|
||||
: typeof (testState.gatewayAuth as { token?: unknown } | undefined)?.token === "string"
|
||||
? ((testState.gatewayAuth as { token?: string }).token ?? undefined)
|
||||
: process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const defaultPassword =
|
||||
opts?.skipDefaultAuth === true
|
||||
? undefined
|
||||
: typeof (testState.gatewayAuth as { password?: unknown } | undefined)?.password === "string"
|
||||
? ((testState.gatewayAuth as { password?: string }).password ?? undefined)
|
||||
: process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const token = opts?.token ?? defaultToken;
|
||||
const password = opts?.password ?? defaultPassword;
|
||||
const requestedScopes = Array.isArray(opts?.scopes) ? opts?.scopes : [];
|
||||
const device = (() => {
|
||||
if (opts?.device === null) return undefined;
|
||||
@@ -347,7 +367,7 @@ export async function connectReq(
|
||||
role,
|
||||
scopes: requestedScopes,
|
||||
signedAtMs,
|
||||
token: opts?.token ?? null,
|
||||
token: token ?? null,
|
||||
});
|
||||
return {
|
||||
id: identity.deviceId,
|
||||
@@ -372,10 +392,10 @@ export async function connectReq(
|
||||
role,
|
||||
scopes: opts?.scopes,
|
||||
auth:
|
||||
opts?.token || opts?.password
|
||||
token || password
|
||||
? {
|
||||
token: opts?.token,
|
||||
password: opts?.password,
|
||||
token,
|
||||
password,
|
||||
}
|
||||
: undefined,
|
||||
device,
|
||||
|
||||
@@ -7,6 +7,12 @@ import { createTestRegistry } from "../test-utils/channel-plugins.js";
|
||||
|
||||
installGatewayTestHooks({ scope: "suite" });
|
||||
|
||||
const resolveGatewayToken = (): string => {
|
||||
const token = (testState.gatewayAuth as { token?: string } | undefined)?.token;
|
||||
if (!token) throw new Error("test gateway token missing");
|
||||
return token;
|
||||
};
|
||||
|
||||
describe("POST /tools/invoke", () => {
|
||||
it("invokes a tool and returns {ok:true,result}", async () => {
|
||||
// Allow the sessions_list tool for main agent.
|
||||
@@ -25,10 +31,11 @@ describe("POST /tools/invoke", () => {
|
||||
const server = await startGatewayServer(port, {
|
||||
bind: "loopback",
|
||||
});
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
@@ -105,9 +112,10 @@ describe("POST /tools/invoke", () => {
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
try {
|
||||
const token = resolveGatewayToken();
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
tool: "sessions_list",
|
||||
action: "json",
|
||||
@@ -167,10 +175,11 @@ describe("POST /tools/invoke", () => {
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
@@ -198,10 +207,11 @@ describe("POST /tools/invoke", () => {
|
||||
|
||||
const port = await getFreePort();
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const res = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ tool: "sessions_list", action: "json", args: {}, sessionKey: "main" }),
|
||||
});
|
||||
|
||||
@@ -234,17 +244,18 @@ describe("POST /tools/invoke", () => {
|
||||
const server = await startGatewayServer(port, { bind: "loopback" });
|
||||
|
||||
const payload = { tool: "sessions_list", action: "json", args: {} };
|
||||
const token = resolveGatewayToken();
|
||||
|
||||
const resDefault = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
expect(resDefault.status).toBe(200);
|
||||
|
||||
const resMain = await fetch(`http://127.0.0.1:${port}/tools/invoke`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
||||
body: JSON.stringify({ ...payload, sessionKey: "main" }),
|
||||
});
|
||||
expect(resMain.status).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user