Spaces:
Paused
Paused
| import { describe, expect, it } from "vitest"; | |
| import crypto from "node:crypto"; | |
| import { authorizeGatewayConnect } from "./auth.js"; | |
| describe("gateway auth", () => { | |
| it("does not throw when req is missing socket", async () => { | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "secret", allowTailscale: false }, | |
| connectAuth: { token: "secret" }, | |
| // Regression: avoid crashing on req.socket.remoteAddress when callers pass a non-IncomingMessage. | |
| req: {} as never, | |
| }); | |
| expect(res.ok).toBe(true); | |
| }); | |
| it("reports missing and mismatched token reasons", async () => { | |
| const missing = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "secret", allowTailscale: false }, | |
| connectAuth: null, | |
| }); | |
| expect(missing.ok).toBe(false); | |
| expect(missing.reason).toBe("token_missing"); | |
| const mismatch = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "secret", allowTailscale: false }, | |
| connectAuth: { token: "wrong" }, | |
| }); | |
| expect(mismatch.ok).toBe(false); | |
| expect(mismatch.reason).toBe("token_mismatch"); | |
| }); | |
| it("reports missing token config reason", async () => { | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "token", allowTailscale: false }, | |
| connectAuth: { token: "anything" }, | |
| }); | |
| expect(res.ok).toBe(false); | |
| expect(res.reason).toBe("token_missing_config"); | |
| }); | |
| it("reports missing and mismatched password reasons", async () => { | |
| const missing = await authorizeGatewayConnect({ | |
| auth: { mode: "password", password: "secret", allowTailscale: false }, | |
| connectAuth: null, | |
| }); | |
| expect(missing.ok).toBe(false); | |
| expect(missing.reason).toBe("password_missing"); | |
| const mismatch = await authorizeGatewayConnect({ | |
| auth: { mode: "password", password: "secret", allowTailscale: false }, | |
| connectAuth: { password: "wrong" }, | |
| }); | |
| expect(mismatch.ok).toBe(false); | |
| expect(mismatch.reason).toBe("password_mismatch"); | |
| }); | |
| it("reports missing password config reason", async () => { | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "password", allowTailscale: false }, | |
| connectAuth: { password: "secret" }, | |
| }); | |
| expect(res.ok).toBe(false); | |
| expect(res.reason).toBe("password_missing_config"); | |
| }); | |
| it("treats local tailscale serve hostnames as direct", async () => { | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "secret", allowTailscale: true }, | |
| connectAuth: { token: "secret" }, | |
| req: { | |
| socket: { remoteAddress: "127.0.0.1" }, | |
| headers: { host: "gateway.tailnet-1234.ts.net:443" }, | |
| } as never, | |
| }); | |
| expect(res.ok).toBe(true); | |
| expect(res.method).toBe("token"); | |
| }); | |
| it("allows tailscale identity to satisfy token mode auth", async () => { | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "secret", allowTailscale: true }, | |
| connectAuth: null, | |
| tailscaleWhois: async () => ({ login: "peter", name: "Peter" }), | |
| req: { | |
| socket: { remoteAddress: "127.0.0.1" }, | |
| headers: { | |
| host: "gateway.local", | |
| "x-forwarded-for": "100.64.0.1", | |
| "x-forwarded-proto": "https", | |
| "x-forwarded-host": "ai-hub.bone-egret.ts.net", | |
| "tailscale-user-login": "peter", | |
| "tailscale-user-name": "Peter", | |
| }, | |
| } as never, | |
| }); | |
| expect(res.ok).toBe(true); | |
| expect(res.method).toBe("tailscale"); | |
| expect(res.user).toBe("peter"); | |
| }); | |
| it("allows Control UI oauth session cookie to satisfy token mode auth", async () => { | |
| const prevEnv = { | |
| OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_ID: process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_ID, | |
| OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_SECRET: process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_SECRET, | |
| OPENCLAW_CONTROL_UI_SESSION_SECRET: process.env.OPENCLAW_CONTROL_UI_SESSION_SECRET, | |
| OPENCLAW_CONTROL_UI_ALLOWED_GOOGLE_EMAILS: process.env.OPENCLAW_CONTROL_UI_ALLOWED_GOOGLE_EMAILS, | |
| }; | |
| try { | |
| process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_ID = "cid"; | |
| process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_SECRET = "csecret"; | |
| process.env.OPENCLAW_CONTROL_UI_SESSION_SECRET = "session-secret"; | |
| process.env.OPENCLAW_CONTROL_UI_ALLOWED_GOOGLE_EMAILS = "admin@example.com"; | |
| const payload = { | |
| v: 1, | |
| exp: Math.floor(Date.now() / 1000) + 60, | |
| provider: "google", | |
| sub: "sub-1", | |
| email: "admin@example.com", | |
| }; | |
| const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url"); | |
| const sig = crypto.createHmac("sha256", "session-secret").update(body).digest("base64url"); | |
| const cookieToken = `${body}.${sig}`; | |
| const res = await authorizeGatewayConnect({ | |
| auth: { mode: "token", token: "gateway-token", allowTailscale: false }, | |
| connectAuth: null, | |
| client: { id: "openclaw-control-ui", version: "test", platform: "web", mode: "webchat" }, | |
| req: { | |
| socket: { remoteAddress: "203.0.113.1" }, | |
| headers: { host: "example.com", cookie: `openclaw_ui_session=${encodeURIComponent(cookieToken)}` }, | |
| } as never, | |
| }); | |
| expect(res.ok).toBe(true); | |
| expect(res.method).toBe("control-ui-oauth"); | |
| expect(res.user).toBe("admin@example.com"); | |
| } finally { | |
| process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_ID = prevEnv.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_ID; | |
| process.env.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_SECRET = prevEnv.OPENCLAW_CONTROL_UI_GOOGLE_CLIENT_SECRET; | |
| process.env.OPENCLAW_CONTROL_UI_SESSION_SECRET = prevEnv.OPENCLAW_CONTROL_UI_SESSION_SECRET; | |
| process.env.OPENCLAW_CONTROL_UI_ALLOWED_GOOGLE_EMAILS = prevEnv.OPENCLAW_CONTROL_UI_ALLOWED_GOOGLE_EMAILS; | |
| } | |
| }); | |
| }); | |