mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(gateway): avoid echoing rotated device tokens
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/device tokens: stop echoing rotated bearer tokens from shared/admin `device.token.rotate` responses while preserving the same-device token handoff needed by token-only clients before reconnect. (#66773) Thanks @MoerAI.
|
||||
- Agents/subagents: enforce `subagents.allowAgents` for explicit same-agent `sessions_spawn(agentId=...)` calls instead of auto-allowing requester self-targets. Fixes #72827. Thanks @oiGaDio.
|
||||
- ACP/sessions_spawn: let explicit `sessions_spawn(runtime="acp")` bootstrap turns run while `acp.dispatch.enabled=false` still blocks automatic ACP thread dispatch. Fixes #63591. Thanks @moeedahmed.
|
||||
- Gateway: skip CLI startup self-respawn for foreground gateway runs so low-memory Linux/Node 24 hosts start through the same path as direct `dist/index.js` without hanging before logs. Fixes #72720. Thanks @sign-2025.
|
||||
|
||||
@@ -102,7 +102,10 @@ caller already has.
|
||||
openclaw devices rotate --device <deviceId> --role operator --scope operator.read --scope operator.write
|
||||
```
|
||||
|
||||
Returns the new token payload as JSON.
|
||||
Returns rotation metadata as JSON. If the caller is rotating its own token while
|
||||
authenticated with that device token, the response also includes the replacement
|
||||
token so the client can persist it before reconnecting. Shared/admin rotations
|
||||
do not echo the bearer token.
|
||||
|
||||
### `openclaw devices revoke --device <id> --role <role>`
|
||||
|
||||
|
||||
@@ -553,6 +553,10 @@ rather than the pre-handshake defaults.
|
||||
reused when the client is reusing the stored per-device token.
|
||||
- Device tokens can be rotated/revoked via `device.token.rotate` and
|
||||
`device.token.revoke` (requires `operator.pairing` scope).
|
||||
- `device.token.rotate` returns rotation metadata. It echoes the replacement
|
||||
bearer token only for same-device calls that are already authenticated with
|
||||
that device token, so token-only clients can persist their replacement before
|
||||
reconnecting. Shared/admin rotations do not echo the bearer token.
|
||||
- Token issuance, rotation, and revocation stay bounded to the approved role set
|
||||
recorded in that device's pairing entry; token mutation cannot expand or
|
||||
target a device role that pairing approval never granted.
|
||||
|
||||
@@ -294,7 +294,6 @@ describe("deviceHandlers", () => {
|
||||
{
|
||||
deviceId: " device-1 ",
|
||||
role: "operator",
|
||||
token: "new-token",
|
||||
scopes: ["operator.pairing"],
|
||||
rotatedAtMs: 789,
|
||||
},
|
||||
@@ -336,6 +335,37 @@ describe("deviceHandlers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("omits rotated tokens when an admin rotates another device token", async () => {
|
||||
mockPairedOperatorDevice();
|
||||
mockRotateOperatorTokenSuccess();
|
||||
const opts = createOptions(
|
||||
"device.token.rotate",
|
||||
{
|
||||
deviceId: "device-1",
|
||||
role: "operator",
|
||||
scopes: ["operator.pairing"],
|
||||
},
|
||||
{
|
||||
client: createClient(["operator.admin", "operator.pairing"], "admin-device", {
|
||||
isDeviceTokenAuth: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
await deviceHandlers["device.token.rotate"](opts);
|
||||
|
||||
expect(opts.respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
{
|
||||
deviceId: "device-1",
|
||||
role: "operator",
|
||||
scopes: ["operator.pairing"],
|
||||
rotatedAtMs: 789,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects rotating a token for a role that was never approved", async () => {
|
||||
mockPairedOperatorDevice();
|
||||
rotateDeviceTokenMock.mockResolvedValue({ ok: false, reason: "unknown-device-or-role" });
|
||||
|
||||
@@ -109,6 +109,10 @@ function deniesCrossDeviceManagement(authz: DeviceManagementAuthz): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function shouldReturnRotatedDeviceToken(authz: DeviceManagementAuthz): boolean {
|
||||
return Boolean(authz.callerDeviceId && authz.callerDeviceId === authz.normalizedTargetDeviceId);
|
||||
}
|
||||
|
||||
export const deviceHandlers: GatewayRequestHandlers = {
|
||||
"device.pair.list": async ({ params, respond, client }) => {
|
||||
if (!validateDevicePairListParams(params)) {
|
||||
@@ -367,7 +371,7 @@ export const deviceHandlers: GatewayRequestHandlers = {
|
||||
{
|
||||
deviceId,
|
||||
role: entry.role,
|
||||
token: entry.token,
|
||||
...(shouldReturnRotatedDeviceToken(authz) ? { token: entry.token } : {}),
|
||||
scopes: entry.scopes,
|
||||
rotatedAtMs: entry.rotatedAtMs ?? entry.createdAtMs,
|
||||
},
|
||||
|
||||
@@ -176,13 +176,20 @@ describe("gateway device.token.rotate/revoke ownership guard (IDOR)", () => {
|
||||
try {
|
||||
await connectOk(started.ws);
|
||||
|
||||
const rotate = await rpcReq<{ token?: string }>(started.ws, "device.token.rotate", {
|
||||
deviceId: device.deviceId,
|
||||
role: "operator",
|
||||
scopes: ["operator.pairing"],
|
||||
});
|
||||
const rotate = await rpcReq<{ rotatedAtMs?: number; token?: string }>(
|
||||
started.ws,
|
||||
"device.token.rotate",
|
||||
{
|
||||
deviceId: device.deviceId,
|
||||
role: "operator",
|
||||
scopes: ["operator.pairing"],
|
||||
},
|
||||
);
|
||||
expect(rotate.ok).toBe(true);
|
||||
expect(rotate.payload?.token).toBeTruthy();
|
||||
expect(rotate.payload?.rotatedAtMs).toBeTypeOf("number");
|
||||
expect(rotate.payload?.token).toBeUndefined();
|
||||
const pairedAfterRotate = await getPairedDevice(device.deviceId);
|
||||
expect(pairedAfterRotate?.tokens?.operator?.token).toBeTruthy();
|
||||
|
||||
const revoke = await rpcReq<{ revokedAtMs?: number }>(started.ws, "device.token.revoke", {
|
||||
deviceId: device.deviceId,
|
||||
|
||||
@@ -115,7 +115,7 @@ export async function rotateDeviceToken(
|
||||
}
|
||||
try {
|
||||
const res = await state.client.request<{
|
||||
token: string;
|
||||
token?: string;
|
||||
role?: string;
|
||||
deviceId?: string;
|
||||
scopes?: Array<string>;
|
||||
|
||||
Reference in New Issue
Block a user