Skip to content

Commit 1a7612f

Browse files
author
hclsys
committed
fix(auth): surface distinct scope-mismatch reason for device token failures
When verifyDeviceToken returns reason="scope-mismatch" (the device token's approved scopes do not cover the requested connection scopes), the auth pipeline previously collapsed this into the generic device_token_mismatch error, causing clients to rotate/reissue a perfectly valid token. Surface a distinct device_token_scope_mismatch reason and error message ("re-pair this device to request the current scope set") so clients can take the right corrective action. Adds AUTH_DEVICE_TOKEN_SCOPE_MISMATCH error detail code for protocol-level classification. Fixes #79292.
1 parent 2d65908 commit 1a7612f

5 files changed

Lines changed: 32 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- Gateway/auth: surface a distinct `device_token_scope_mismatch` reason and error message when a device token's approved scopes do not cover the requested connection scopes, instead of collapsing all `verifyDeviceToken` failures into the generic `device_token_mismatch` error. Fixes #79292.
910
- Google/Gemini: normalize retired `google/gemini-3-pro-preview` and `google-gemini-cli/gemini-3-pro-preview` selections to `google/gemini-3.1-pro-preview` before they are written to model config.
1011
- Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack.
1112
- Control UI/usage: add transcript-backed historical lineage rollups for rotated logical sessions, with current-instance vs historical-lineage scope controls and long-range presets so usage history stays visible after restarts and updates. Fixes #50701. Thanks @dev-gideon-llc and @BunsDev.

src/gateway/protocol/connect-error-details.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const ConnectErrorDetailCodes = {
1111
AUTH_PASSWORD_NOT_CONFIGURED: "AUTH_PASSWORD_NOT_CONFIGURED", // pragma: allowlist secret
1212
AUTH_BOOTSTRAP_TOKEN_INVALID: "AUTH_BOOTSTRAP_TOKEN_INVALID",
1313
AUTH_DEVICE_TOKEN_MISMATCH: "AUTH_DEVICE_TOKEN_MISMATCH",
14+
AUTH_DEVICE_TOKEN_SCOPE_MISMATCH: "AUTH_DEVICE_TOKEN_SCOPE_MISMATCH",
1415
AUTH_RATE_LIMITED: "AUTH_RATE_LIMITED",
1516
AUTH_TAILSCALE_IDENTITY_MISSING: "AUTH_TAILSCALE_IDENTITY_MISSING",
1617
AUTH_TAILSCALE_PROXY_MISSING: "AUTH_TAILSCALE_PROXY_MISSING",
@@ -158,6 +159,8 @@ export function resolveAuthConnectErrorDetailCode(
158159
return ConnectErrorDetailCodes.AUTH_RATE_LIMITED;
159160
case "device_token_mismatch":
160161
return ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_MISMATCH;
162+
case "device_token_scope_mismatch":
163+
return ConnectErrorDetailCodes.AUTH_DEVICE_TOKEN_SCOPE_MISMATCH;
161164
case undefined:
162165
return ConnectErrorDetailCodes.AUTH_REQUIRED;
163166
default:

src/gateway/server/ws-connection/auth-context.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ describe("resolveConnectAuthDecision", () => {
108108
expect(verifyDeviceToken).toHaveBeenCalledOnce();
109109
});
110110

111+
it("reports scope-mismatch from verifyDeviceToken as device_token_scope_mismatch", async () => {
112+
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({
113+
ok: false,
114+
reason: "scope-mismatch",
115+
}));
116+
const decision = await resolveConnectAuthDecision({
117+
state: createBaseState({
118+
deviceTokenCandidateSource: "explicit-device-token",
119+
}),
120+
hasDeviceIdentity: true,
121+
deviceId: "dev-1",
122+
publicKey: "pub-1",
123+
role: "operator",
124+
scopes: ["operator.read", "operator.admin", "operator.pairing"],
125+
verifyBootstrapToken: async () => ({ ok: false, reason: "bootstrap_token_invalid" }),
126+
verifyDeviceToken,
127+
});
128+
expect(decision.authOk).toBe(false);
129+
expect(decision.authResult.reason).toBe("device_token_scope_mismatch");
130+
});
131+
111132
it("reports explicit device-token mismatches as device_token_mismatch", async () => {
112133
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: false }));
113134
const decision = await resolveConnectAuthDecision({

src/gateway/server/ws-connection/auth-context.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export type ConnectAuthState = {
3232
deviceTokenCandidateSource?: DeviceTokenCandidateSource;
3333
};
3434

35-
type VerifyDeviceTokenResult = { ok: boolean };
35+
type VerifyDeviceTokenResult = { ok: boolean; reason?: string };
3636
type VerifyBootstrapTokenResult = { ok: boolean; reason?: string };
3737

3838
export type ConnectAuthDecision = {
@@ -214,10 +214,12 @@ export async function resolveConnectAuthDecision(params: {
214214
params.rateLimiter?.reset(params.clientIp, AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
215215
}
216216
} else {
217+
const isScopeMismatch = tokenCheck.reason === "scope-mismatch";
217218
authResult = {
218219
ok: false,
219-
reason:
220-
params.state.deviceTokenCandidateSource === "explicit-device-token"
220+
reason: isScopeMismatch
221+
? "device_token_scope_mismatch"
222+
: params.state.deviceTokenCandidateSource === "explicit-device-token"
221223
? "device_token_mismatch"
222224
: (authResult.reason ?? "device_token_mismatch"),
223225
};

src/gateway/server/ws-connection/auth-messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export function formatGatewayAuthFailureMessage(params: {
5555
return "unauthorized: too many failed authentication attempts (retry later)";
5656
case "device_token_mismatch":
5757
return "unauthorized: device token mismatch (rotate/reissue device token)";
58+
case "device_token_scope_mismatch":
59+
return "unauthorized: device token scope mismatch (re-pair this device to request the current scope set)";
5860
default:
5961
break;
6062
}

0 commit comments

Comments
 (0)