Skip to content

fix(gateway): include device token for fallback auth when shared token is configured#39002

Closed
widingmarcus-cyber wants to merge 1 commit intoopenclaw:mainfrom
widingmarcus-cyber:fix/gateway-auth-device-token-fallback-38617
Closed

fix(gateway): include device token for fallback auth when shared token is configured#39002
widingmarcus-cyber wants to merge 1 commit intoopenclaw:mainfrom
widingmarcus-cyber:fix/gateway-auth-device-token-fallback-38617

Conversation

@widingmarcus-cyber
Copy link
Copy Markdown
Contributor

Problem

When gateway.auth.token is manually configured in openclaw.json, the CLI cannot connect to the Gateway with token_mismatch error, even though the device was successfully paired via openclaw pair.

Root Cause: The client was excluding the stored device token from auth.deviceToken when an explicit shared token was provided. This prevented the server from falling back to device-token authentication when shared token validation failed.

Impact: Paired CLI devices cannot authenticate when gateway.auth.token is manually configured, breaking all CLI commands.

Root Cause

In src/gateway/client.ts, the resolvedDeviceToken was set to undefined when explicitGatewayToken was present:

// Before (broken):
const resolvedDeviceToken =
  explicitDeviceToken ?? (!explicitGatewayToken ? (storedToken ?? undefined) : undefined);

This meant auth.deviceToken was never sent to the server when a shared token was configured, so the server couldn't use device-token auth as a fallback.

Fix

Always include the stored device token when available:

// After (fixed):
const resolvedDeviceToken = explicitDeviceToken ?? (storedToken ?? undefined);

The server already supports using auth.deviceToken as a fallback when auth.token fails shared auth validation. This fix ensures the client sends both tokens, allowing the server to authenticate via the device token when the shared token doesn't match.

Testing

  • Updated existing test to verify the new behavior
  • All gateway auth tests pass (pnpm test -- src/gateway/client.test.ts src/gateway/auth.test.ts src/gateway/server/ws-connection/auth-context.test.ts)

Fixes #38617

@greptile-apps please review

…n is configured

When gateway.auth.token is manually configured in openclaw.json, the client
was not including the device token in auth.deviceToken for the server to use
as a fallback. This caused authentication failures with 'token_mismatch' errors
because:

1. User configures gateway.auth.token in openclaw.json
2. User pairs CLI device (openclaw pair) → device gets unique token in paired.json
3. CLI sends the configured gateway.auth.token, but this mismatches the paired
   device token stored on the server
4. Server cannot fall back to device token verification because auth.deviceToken
   was undefined

The fix ensures the stored device token is always included in auth.deviceToken,
allowing the server to use device-token authentication as a fallback when shared
token auth fails.

Fixes openclaw#38617
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 7, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Stored per-device token is always sent in connect auth payload (token exfiltration to untrusted gateway URL)

1. 🟡 Stored per-device token is always sent in connect auth payload (token exfiltration to untrusted gateway URL)

Property Value
Severity Medium
CWE CWE-201
Location src/gateway/client.ts:250-270

Description

The Gateway client now always includes a locally persisted per-device token (device-auth.json) in the WebSocket connect auth payload, even when an explicit shared gateway token is configured.

This is a security regression because it unnecessarily discloses a long-lived bearer credential to whatever URL the client is configured to connect to:

  • Input/source: storedToken is loaded from local persistent storage (loadDeviceAuthToken(...)), representing a per-device credential for a previously paired gateway.
  • Sink: The token is embedded into params.auth.deviceToken and transmitted over the network in the connect request frame.
  • Impact: If the client is pointed (by configuration, DNS compromise, social engineering, or other misrouting) at a malicious/compromised gateway endpoint, that endpoint can harvest the device token and potentially replay it to the original gateway (tokens are not bound to a gateway origin in this client-side store).

Vulnerable code:

const storedToken = this.opts.deviceIdentity
  ? loadDeviceAuthToken({ deviceId: this.opts.deviceIdentity.deviceId, role })?.token
  : null;// Always include the device token when available.
const resolvedDeviceToken = explicitDeviceToken ?? storedToken ?? undefined;
...
const auth = authToken || authPassword || resolvedDeviceToken
  ? { token: authToken, deviceToken: resolvedDeviceToken, password: authPassword }
  : undefined;

Note: While the client blocks plaintext ws:// to non-loopback by default, this does not address the main issue here: disclosure of an unnecessary credential to an arbitrary (potentially attacker-controlled) wss:// endpoint.

Recommendation

Avoid sending a persisted per-device token unless it is strictly required and/or the destination is explicitly trusted.

Mitigation options (choose one or combine):

  1. Revert to least-privilege behavior: only include the stored device token when no explicit shared token/password is configured.
const resolvedDeviceToken =
  explicitDeviceToken ?? (!explicitGatewayToken && !authPassword ? (storedToken ?? undefined) : undefined);
  1. Two-step fallback: first attempt connect with shared credentials only; if the server responds with an auth failure that is safe to treat as “try device token”, reconnect and include deviceToken.

  2. Bind stored device tokens to gateway identity (recommended): store tokens keyed by a stable gateway identifier (e.g., host + pinned TLS fingerprint / server public key) and only load/send a token when connecting to the same gateway identity.

Additionally, consider an explicit option like sendStoredDeviceTokenFallback: boolean defaulting to false when opts.token is present.


Analyzed PR: #39002 at commit e35114c

Last updated on: 2026-03-07T17:12:39Z

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: XS labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR fixes a bug where paired CLI devices could not authenticate against a gateway with a manually configured gateway.auth.token, by ensuring auth.deviceToken is always populated from the stored per-device token when available — regardless of whether an explicit shared token is also being sent. The server already supported device-token fallback auth when shared token validation failed; the client was simply not sending the required field.

The one-line change to client.ts is correct and aligns with the server-side auth logic. The updated test correctly exercises the fixed code path and should pass as written.

Confidence Score: 4/5

  • The production fix is minimal and well-reasoned; the test correctly validates the behavior without structural issues.
  • The one-line change in client.ts is a straightforward, correct fix that aligns with the server-side fallback logic documented in the PR. The test validates that both token and deviceToken fields are correctly populated in the auth payload. The test's omission of an explicit deviceIdentity option is not a problem — the constructor automatically provides one via loadOrCreateDeviceIdentity(), ensuring the stored device token is loaded and included. No structural or logical issues remain.
  • No files require special attention

Last reviewed commit: e35114c

@joshavant
Copy link
Copy Markdown
Contributor

Thanks for the contribution here.

We merged #42507, which supersedes this PR with a broader implementation: bounded trusted retry, reconnect-loop gating, protocol recovery hints, cross-client alignment, regression coverage, and docs/runbook updates.

Closing this PR as superseded by the merged follow-up.

@joshavant joshavant closed this Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gateway CLI authentication fails with token_mismatch when gateway.auth.token is configured

2 participants