fix(security): harden hook and device token auth

This commit is contained in:
Peter Steinberger
2026-02-13 01:23:26 +01:00
parent 54513f4240
commit 113ebfd6a2
9 changed files with 190 additions and 12 deletions

View File

@@ -7,6 +7,7 @@ import {
getPairedDevice,
requestDevicePairing,
rotateDeviceToken,
verifyDeviceToken,
} from "./device-pairing.js";
describe("device pairing tokens", () => {
@@ -41,4 +42,40 @@ describe("device pairing tokens", () => {
paired = await getPairedDevice("device-1", baseDir);
expect(paired?.tokens?.operator?.scopes).toEqual(["operator.read"]);
});
test("verifies token and rejects mismatches", async () => {
const baseDir = await mkdtemp(join(tmpdir(), "openclaw-device-pairing-"));
const request = await requestDevicePairing(
{
deviceId: "device-1",
publicKey: "public-key-1",
role: "operator",
scopes: ["operator.read"],
},
baseDir,
);
await approveDevicePairing(request.request.requestId, baseDir);
const paired = await getPairedDevice("device-1", baseDir);
const token = paired?.tokens?.operator?.token;
expect(token).toBeTruthy();
const ok = await verifyDeviceToken({
deviceId: "device-1",
token: token ?? "",
role: "operator",
scopes: ["operator.read"],
baseDir,
});
expect(ok.ok).toBe(true);
const mismatch = await verifyDeviceToken({
deviceId: "device-1",
token: "x".repeat((token ?? "1234").length),
role: "operator",
scopes: ["operator.read"],
baseDir,
});
expect(mismatch.ok).toBe(false);
expect(mismatch.reason).toBe("token-mismatch");
});
});

View File

@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import { safeEqualSecret } from "../security/secret-equal.js";
export type DevicePairingPendingRequest = {
requestId: string;
@@ -431,7 +432,7 @@ export async function verifyDeviceToken(params: {
if (entry.revokedAtMs) {
return { ok: false, reason: "token-revoked" };
}
if (entry.token !== params.token) {
if (!safeEqualSecret(params.token, entry.token)) {
return { ok: false, reason: "token-mismatch" };
}
const requestedScopes = normalizeScopes(params.scopes);