mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): align control-ui device auth token signing
This commit is contained in:
@@ -75,30 +75,6 @@ vi.mock("./device-identity.ts", () => ({
|
||||
|
||||
const { GatewayBrowserClient } = await import("./gateway.ts");
|
||||
|
||||
function createStorageMock(): Storage {
|
||||
const store = new Map<string, string>();
|
||||
return {
|
||||
get length() {
|
||||
return store.size;
|
||||
},
|
||||
clear() {
|
||||
store.clear();
|
||||
},
|
||||
getItem(key: string) {
|
||||
return store.get(key) ?? null;
|
||||
},
|
||||
key(index: number) {
|
||||
return Array.from(store.keys())[index] ?? null;
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key);
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(key, String(value));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getLatestWebSocket(): MockWebSocket {
|
||||
const ws = wsInstances.at(-1);
|
||||
if (!ws) {
|
||||
@@ -118,23 +94,8 @@ describe("GatewayBrowserClient", () => {
|
||||
publicKey: "public-key", // pragma: allowlist secret
|
||||
});
|
||||
|
||||
const localStorage = createStorageMock();
|
||||
window.localStorage.clear();
|
||||
vi.stubGlobal("WebSocket", MockWebSocket);
|
||||
vi.stubGlobal("localStorage", localStorage);
|
||||
vi.stubGlobal("crypto", {
|
||||
randomUUID: vi.fn(() => "req-1"),
|
||||
subtle: {},
|
||||
});
|
||||
vi.stubGlobal("navigator", {
|
||||
language: "en-GB",
|
||||
platform: "test-platform",
|
||||
userAgent: "test-agent",
|
||||
});
|
||||
vi.stubGlobal("window", {
|
||||
clearTimeout: vi.fn(),
|
||||
localStorage,
|
||||
setTimeout: vi.fn(() => 1),
|
||||
});
|
||||
|
||||
storeDeviceAuthToken({
|
||||
deviceId: "device-1",
|
||||
@@ -148,7 +109,7 @@ describe("GatewayBrowserClient", () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("keeps shared auth token separate from cached device token", async () => {
|
||||
it("prefers explicit shared auth over cached device tokens", async () => {
|
||||
const client = new GatewayBrowserClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "shared-auth-token",
|
||||
@@ -162,19 +123,47 @@ describe("GatewayBrowserClient", () => {
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "nonce-1" },
|
||||
});
|
||||
await Promise.resolve();
|
||||
await vi.waitFor(() => expect(ws.sent.length).toBeGreaterThan(0));
|
||||
|
||||
const connectFrame = JSON.parse(ws.sent.at(-1) ?? "{}") as {
|
||||
id?: string;
|
||||
method?: string;
|
||||
params?: { auth?: { token?: string } };
|
||||
};
|
||||
expect(connectFrame.id).toBe("req-1");
|
||||
expect(typeof connectFrame.id).toBe("string");
|
||||
expect(connectFrame.method).toBe("connect");
|
||||
expect(connectFrame.params?.auth?.token).toBe("shared-auth-token");
|
||||
expect(signDevicePayloadMock).toHaveBeenCalledWith("private-key", expect.any(String));
|
||||
const signedPayload = signDevicePayloadMock.mock.calls[0]?.[1];
|
||||
expect(signedPayload).toContain("|shared-auth-token|nonce-1");
|
||||
expect(signedPayload).not.toContain("stored-device-token");
|
||||
});
|
||||
|
||||
it("uses cached device tokens only when no explicit shared auth is provided", async () => {
|
||||
const client = new GatewayBrowserClient({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
});
|
||||
|
||||
client.start();
|
||||
const ws = getLatestWebSocket();
|
||||
ws.emitOpen();
|
||||
ws.emitMessage({
|
||||
type: "event",
|
||||
event: "connect.challenge",
|
||||
payload: { nonce: "nonce-1" },
|
||||
});
|
||||
await vi.waitFor(() => expect(ws.sent.length).toBeGreaterThan(0));
|
||||
|
||||
const connectFrame = JSON.parse(ws.sent.at(-1) ?? "{}") as {
|
||||
id?: string;
|
||||
method?: string;
|
||||
params?: { auth?: { token?: string } };
|
||||
};
|
||||
expect(typeof connectFrame.id).toBe("string");
|
||||
expect(connectFrame.method).toBe("connect");
|
||||
expect(connectFrame.params?.auth?.token).toBe("stored-device-token");
|
||||
expect(signDevicePayloadMock).toHaveBeenCalledWith("private-key", expect.any(String));
|
||||
const signedPayload = signDevicePayloadMock.mock.calls[0]?.[1];
|
||||
expect(signedPayload).toContain("|stored-device-token|nonce-1");
|
||||
expect(signedPayload).not.toContain("shared-auth-token");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,17 +205,22 @@ export class GatewayBrowserClient {
|
||||
const role = "operator";
|
||||
let deviceIdentity: Awaited<ReturnType<typeof loadOrCreateDeviceIdentity>> | null = null;
|
||||
let canFallbackToShared = false;
|
||||
let authToken = this.opts.token;
|
||||
const explicitGatewayToken = this.opts.token?.trim() || undefined;
|
||||
let authToken = explicitGatewayToken;
|
||||
let deviceToken: string | undefined;
|
||||
|
||||
if (isSecureContext) {
|
||||
deviceIdentity = await loadOrCreateDeviceIdentity();
|
||||
deviceToken = loadDeviceAuthToken({
|
||||
const storedToken = loadDeviceAuthToken({
|
||||
deviceId: deviceIdentity.deviceId,
|
||||
role,
|
||||
})?.token;
|
||||
canFallbackToShared = Boolean(deviceToken && this.opts.token);
|
||||
deviceToken = !(explicitGatewayToken || this.opts.password?.trim())
|
||||
? (storedToken ?? undefined)
|
||||
: undefined;
|
||||
canFallbackToShared = Boolean(deviceToken && explicitGatewayToken);
|
||||
}
|
||||
authToken = explicitGatewayToken ?? deviceToken;
|
||||
const auth =
|
||||
authToken || this.opts.password
|
||||
? {
|
||||
@@ -244,7 +249,7 @@ export class GatewayBrowserClient {
|
||||
role,
|
||||
scopes,
|
||||
signedAtMs,
|
||||
token: deviceToken ?? null,
|
||||
token: authToken ?? null,
|
||||
nonce,
|
||||
});
|
||||
const signature = await signDevicePayload(deviceIdentity.privateKey, payload);
|
||||
|
||||
Reference in New Issue
Block a user