fix(auth): propagate scope-mismatch reason instead of collapsing to device_token_mismatch#79314
Conversation
…evice_token_mismatch
When verifyDeviceToken returns { ok: false, reason: 'scope-mismatch' },
resolveConnectAuthDecision was discarding that reason and always emitting
'device_token_mismatch'. This caused the gateway to tell the user their
token value was wrong (rotate/reissue) when the real problem was that the
connected client requested scopes beyond what the stored device token
allows (e.g. operator.admin / operator.pairing which bootstrap tokens
do not include).
Changes:
- auth-context.ts: propagate scope-mismatch reason as scope_mismatch
(also widens VerifyDeviceTokenResult to include optional reason)
- connect-error-details.ts: add AUTH_DEVICE_TOKEN_SCOPE_MISMATCH code
and map scope_mismatch in resolveAuthConnectErrorDetailCode
- auth-messages.ts: human-readable message directing users to re-pair
or approve a scope upgrade
- auth-context.test.ts: regression test for scope-mismatch propagation
Fixes openclaw#79292
|
Codex review: needs changes before merge. Summary Reproducibility: yes. Current main's verifier returns Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Merge a revised version that propagates scope mismatch end-to-end, including client pause/error handling and regression coverage that preserves stored tokens for scope mismatches. Do we have a high-confidence way to reproduce the issue? Yes. Current main's verifier returns Is this the best way to solve the issue? No, not yet. Server-side propagation is the right direction, but the new protocol detail code needs matching handling in the TypeScript, Swift, and Android clients so reconnect and user guidance behavior remains correct. Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against a8c745a6230e. |
|
Thanks for the focused fix here. This has now been superseded by the broader maintainer landing in #80189, merged as 51b73b3. That landed the same diagnostic preservation goal and also covered the bootstrap/cached device-token scope contract plus reconnect-loop behavior for |
Preserve full shared-token operator sessions while reusing bootstrap/cached device-token scopes for handoff reconnects. Surface device-token scope mismatches as AUTH_SCOPE_MISMATCH and stop reconnect retry loops without clearing valid stored tokens. Fixes openclaw#79292. Supersedes openclaw#79314, openclaw#79296, openclaw#79295.
Preserve full shared-token operator sessions while reusing bootstrap/cached device-token scopes for handoff reconnects. Surface device-token scope mismatches as AUTH_SCOPE_MISMATCH and stop reconnect retry loops without clearing valid stored tokens. Fixes openclaw#79292. Supersedes openclaw#79314, openclaw#79296, openclaw#79295.
Preserve full shared-token operator sessions while reusing bootstrap/cached device-token scopes for handoff reconnects. Surface device-token scope mismatches as AUTH_SCOPE_MISMATCH and stop reconnect retry loops without clearing valid stored tokens. Fixes openclaw#79292. Supersedes openclaw#79314, openclaw#79296, openclaw#79295.
Summary
Fixes #79292
When
verifyDeviceTokenreturns{ ok: false, reason: 'scope-mismatch' },resolveConnectAuthDecisionwas discarding that reason and always emittingdevice_token_mismatch. This caused the gateway to show users a misleading error ("rotate/reissue device token") when the actual problem is that the connecting client requested scopes beyond what the stored device token allows — specificallyoperator.adminandoperator.pairingwhich bootstrap tokens do not include.Root cause
VerifyDeviceTokenResultwas typed as{ ok: boolean }with noreasonfield, so the scope-mismatch reason fromverifyDeviceTokenwas silently lost.resolveConnectAuthDecisionthen fell back to the genericdevice_token_mismatchstring for explicit device tokens.Changes
auth-context.ts: WidenVerifyDeviceTokenResultto includereason?: string. When the reason isscope-mismatch, propagate it asscope_mismatchon the auth result instead of using the generic fallback.connect-error-details.ts: AddAUTH_DEVICE_TOKEN_SCOPE_MISMATCHerror code and mapscope_mismatchreason inresolveAuthConnectErrorDetailCode.auth-messages.ts: Add human-readable message forscope_mismatch: "unauthorized: device token scope mismatch (re-pair or approve scope upgrade in Control UI)"auth-context.test.ts: Regression test verifying scope-mismatch propagation.Before / After
operator.adminbut token only has 4 bootstrap scopesdevice_token_mismatch→ "rotate/reissue device token"scope_mismatch→ "device token scope mismatch (re-pair or approve scope upgrade)"device_token_mismatch(correct)device_token_mismatch(unchanged)Testing
Real behavior proof
Executed directly on a running OpenClaw host (Node v25.5.0, 2026-05-08T08:37:58Z). Demonstrates the before/after behavior of the auth decision function:
Scenario A: client requests
operator.admin(scope-mismatch fromverifyDeviceToken)Scenario B: token value is actually wrong (genuine mismatch — unchanged)
User-facing error message:
macOS/iOS/Control UI clients request
operator.admin+operator.pairingon connect, but bootstrap tokens only include 4 scopes. Before this fix,verifyDeviceTokenreturnedscope-mismatchbutresolveConnectAuthDecisiondiscarded it and showed the generic "rotate/reissue device token" message, causing users to delete valid paired devices and re-pair unnecessarily.