mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 21:10:43 +00:00
fix(pairing): restore qr bootstrap onboarding handoff (#58382) (thanks @ngutman)
* fix(pairing): restore qr bootstrap onboarding handoff * fix(pairing): tighten bootstrap handoff follow-ups * fix(pairing): migrate legacy gateway device auth * fix(pairing): narrow qr bootstrap handoff scope * fix(pairing): clear ios tls trust on onboarding reset * fix(pairing): restore qr bootstrap onboarding handoff (#58382) (thanks @ngutman)
This commit is contained in:
@@ -2,7 +2,10 @@ import type { IncomingMessage } from "node:http";
|
||||
import os from "node:os";
|
||||
import type { WebSocket } from "ws";
|
||||
import { loadConfig } from "../../../config/config.js";
|
||||
import { verifyDeviceBootstrapToken } from "../../../infra/device-bootstrap.js";
|
||||
import {
|
||||
revokeDeviceBootstrapToken,
|
||||
verifyDeviceBootstrapToken,
|
||||
} from "../../../infra/device-bootstrap.js";
|
||||
import {
|
||||
deriveDeviceIdFromPublicKey,
|
||||
normalizeDevicePublicKeyBase64Url,
|
||||
@@ -752,6 +755,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
};
|
||||
const requirePairing = async (
|
||||
reason: "not-paired" | "role-upgrade" | "scope-upgrade" | "metadata-upgrade",
|
||||
existingPairedDevice: Awaited<ReturnType<typeof getPairedDevice>> | null = null,
|
||||
) => {
|
||||
const pairingStateAllowsRequestedAccess = (
|
||||
pairedCandidate: Awaited<ReturnType<typeof getPairedDevice>>,
|
||||
@@ -786,11 +790,23 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
isWebchat,
|
||||
reason,
|
||||
});
|
||||
// QR bootstrap onboarding is node-only and single-use. When a fresh device presents
|
||||
// a valid bootstrap token for the baseline node profile, complete pairing in the same
|
||||
// handshake so iOS does not get stuck retrying with an already-consumed bootstrap token.
|
||||
const allowSilentBootstrapPairing =
|
||||
authMethod === "bootstrap-token" &&
|
||||
reason === "not-paired" &&
|
||||
role === "node" &&
|
||||
scopes.length === 0 &&
|
||||
!existingPairedDevice;
|
||||
const pairing = await requestDevicePairing({
|
||||
deviceId: device.id,
|
||||
publicKey: devicePublicKey,
|
||||
...clientPairingMetadata,
|
||||
silent: reason === "scope-upgrade" ? false : allowSilentLocalPairing,
|
||||
silent:
|
||||
reason === "scope-upgrade"
|
||||
? false
|
||||
: allowSilentLocalPairing || allowSilentBootstrapPairing,
|
||||
});
|
||||
const context = buildRequestContext();
|
||||
let approved: Awaited<ReturnType<typeof approveDevicePairing>> | undefined;
|
||||
@@ -815,6 +831,16 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
callerScopes: scopes,
|
||||
});
|
||||
if (approved?.status === "approved") {
|
||||
if (allowSilentBootstrapPairing && bootstrapTokenCandidate) {
|
||||
const revoked = await revokeDeviceBootstrapToken({
|
||||
token: bootstrapTokenCandidate,
|
||||
});
|
||||
if (!revoked.removed) {
|
||||
logGateway.warn(
|
||||
`bootstrap token revoke skipped after silent auto-approval device=${approved.device.deviceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
logGateway.info(
|
||||
`device pairing auto-approved device=${approved.device.deviceId} role=${approved.device.role ?? "unknown"}`,
|
||||
);
|
||||
@@ -879,7 +905,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const paired = await getPairedDevice(device.id);
|
||||
const isPaired = paired?.publicKey === devicePublicKey;
|
||||
if (!isPaired) {
|
||||
const ok = await requirePairing("not-paired");
|
||||
const ok = await requirePairing("not-paired", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
@@ -899,7 +925,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
logGateway.warn(
|
||||
`security audit: device metadata upgrade requested reason=metadata-upgrade device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} payload=${deviceAuthPayloadVersion ?? "unknown"} claimedPlatform=${claimedPlatform ?? "<none>"} pinnedPlatform=${pairedPlatform ?? "<none>"} claimedDeviceFamily=${claimedDeviceFamily ?? "<none>"} pinnedDeviceFamily=${pairedDeviceFamily ?? "<none>"} client=${connectParams.client.id} conn=${connId}`,
|
||||
);
|
||||
const ok = await requirePairing("metadata-upgrade");
|
||||
const ok = await requirePairing("metadata-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
@@ -920,13 +946,13 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const allowedRoles = new Set(pairedRoles);
|
||||
if (allowedRoles.size === 0) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
const ok = await requirePairing("role-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
} else if (!allowedRoles.has(role)) {
|
||||
logUpgradeAudit("role-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("role-upgrade");
|
||||
const ok = await requirePairing("role-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
@@ -935,7 +961,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
if (scopes.length > 0) {
|
||||
if (pairedScopes.length === 0) {
|
||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("scope-upgrade");
|
||||
const ok = await requirePairing("scope-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
@@ -947,7 +973,7 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
});
|
||||
if (!scopesAllowed) {
|
||||
logUpgradeAudit("scope-upgrade", pairedRoles, pairedScopes);
|
||||
const ok = await requirePairing("scope-upgrade");
|
||||
const ok = await requirePairing("scope-upgrade", paired);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user