fix: detect ?token= and suggest #token= fragment syntax

When users visit the Control UI with ?token=<token>, they see
"device identity required" with no hint about the correct URL format.

This change:
- Detects when token is read from query string vs URL fragment
- Warns via console when ?token= is used
- Shows an inline hint in the overview error area directing users
  to use #token=<token> instead

Fixes #54842
This commit is contained in:
jjjojoj
2026-04-04 10:33:47 +08:00
committed by Peter Steinberger
parent cfb7779584
commit 39c721d382
4 changed files with 39 additions and 1 deletions

View File

@@ -12,6 +12,7 @@ import {
renderTopbarThemeModeToggle,
switchChatSession,
} from "./app-render.helpers.ts";
import { warnQueryToken } from "./app-settings.ts";
import type { AppViewState } from "./app-view-state.ts";
import { loadAgentFileContent, loadAgentFiles, saveAgentFile } from "./controllers/agent-files.ts";
import { loadAgentIdentities, loadAgentIdentity } from "./controllers/agent-identity.ts";
@@ -737,6 +738,7 @@ export function renderApp(state: AppViewState) {
cronEnabled: state.cronStatus?.enabled ?? null,
cronNext,
lastChannelsRefresh: state.channelsLastSuccess,
warnQueryToken,
usageResult: state.usageResult,
sessionsResult: state.sessionsResult,
skillsReport: state.skillsReport,

View File

@@ -97,6 +97,9 @@ export function setLastActiveSessionKey(host: SettingsHost, next: string) {
applySettings(host, { ...host.settings, lastActiveSessionKey: trimmed });
}
/** Set to true when the token is read from a query string (?token=) instead of a URL fragment. */
export let warnQueryToken = false;
export function applySettingsFromUrl(host: SettingsHost) {
if (!window.location.search && !window.location.hash) {
return;
@@ -111,7 +114,9 @@ export function applySettingsFromUrl(host: SettingsHost) {
// Prefer fragment tokens over query tokens. Fragments avoid server-side request
// logs and referrer leakage; query-param tokens remain a one-time legacy fallback
// for compatibility with older deep links.
const tokenRaw = hashParams.get("token") ?? params.get("token");
const queryToken = params.get("token");
const hashToken = hashParams.get("token");
const tokenRaw = hashToken ?? queryToken;
const passwordRaw = params.get("password") ?? hashParams.get("password");
const sessionRaw = params.get("session") ?? hashParams.get("session");
const shouldResetSessionForToken = Boolean(
@@ -125,6 +130,12 @@ export function applySettingsFromUrl(host: SettingsHost) {
}
if (tokenRaw != null) {
if (queryToken != null) {
warnQueryToken = true;
console.warn(
"[openclaw] Auth token passed as query parameter (?token=). Use URL fragment instead: #token=<token>. Query parameters may appear in server logs.",
);
}
const token = tokenRaw.trim();
if (token && gatewayUrlChanged) {
host.pendingGatewayToken = token;

View File

@@ -266,6 +266,7 @@ function createOverviewProps(overrides: Partial<OverviewProps> = {}): OverviewPr
cronEnabled: null,
cronNext: null,
lastChannelsRefresh: null,
warnQueryToken: false,
usageResult: null,
sessionsResult: null,
skillsReport: null,

View File

@@ -37,6 +37,7 @@ export type OverviewProps = {
cronEnabled: boolean | null;
cronNext: number | null;
lastChannelsRefresh: number | null;
warnQueryToken: boolean;
// New dashboard data
usageResult: SessionsUsageResult | null;
sessionsResult: SessionsListResult | null;
@@ -191,6 +192,28 @@ export function renderOverview(props: OverviewProps) {
`;
})();
const queryTokenHint = (() => {
if (props.connected || !props.lastError || !props.warnQueryToken) {
return null;
}
const lower = props.lastError.toLowerCase();
const authFailed =
lower.includes("unauthorized") ||
lower.includes("connect failed") ||
lower.includes("device identity required");
if (!authFailed) {
return null;
}
return html`
<div class="muted" style="margin-top: 8px">
Auth token must be passed as a URL fragment:
<span class="mono">#token=&lt;token&gt;</span>. Query parameters (<span class="mono"
>?token=</span
>) may appear in server logs.
</div>
`;
})();
const currentLocale = isSupportedLocale(props.settings.locale)
? props.settings.locale
: i18n.getLocale();
@@ -377,6 +400,7 @@ export function renderOverview(props: OverviewProps) {
? html`<div class="callout danger" style="margin-top: 14px;">
<div>${props.lastError}</div>
${pairingHint ?? ""} ${authHint ?? ""} ${insecureContextHint ?? ""}
${queryTokenHint ?? ""}
</div>`
: html`
<div class="callout" style="margin-top: 14px">