#10382: feat(gateway): server-side token injection for reverse proxy deployments
app: web-ui
gateway
stale
size: S
Cluster:
Gateway Token Management
## Summary
When running OpenClaw behind a reverse proxy (e.g. oauth2-proxy for SSO), the browser needs the gateway token for WebSocket authentication. Currently this requires deploying a custom HTML injection middleware between the SSO proxy and OpenClaw — adding operational complexity and an extra process to manage.
This PR adds a native `gateway.auth.injectTokenFromHeader` config option that:
- Reads the gateway auth token from an HTTP request header (set by the reverse proxy)
- Passes it to the browser via the bootstrap JSON config endpoint (`/__openclaw/control-ui-config.json`)
- The Control UI stores it in `localStorage` and uses it for WebSocket authentication
This eliminates the need for custom token injection proxies in SSO deployments.
## Motivation
In Cloud Foundry (and similar PaaS) deployments, oauth2-proxy handles SSO authentication as a sidecar process. The gateway token must reach the browser for WebSocket auth, but:
- The token can't go in HTTP headers (WS auth is at protocol level, not HTTP upgrade)
- The `?token=` query param approach doesn't work with SSO redirects (proxy can't inject query params)
- The current workaround requires a separate Node.js proxy process that intercepts HTML responses and injects a `<script>` tag — this is fragile and adds ~50 lines of deployment glue
With this change, the reverse proxy just sets an HTTP header (e.g. `--pass-header=X-OpenClaw-Token` in oauth2-proxy), and the gateway handles the rest natively.
## Configuration
```json
{
"gateway": {
"auth": {
"mode": "token",
"token": "my-secret",
"injectTokenFromHeader": {
"enabled": true,
"headerName": "x-openclaw-token"
}
}
}
}
```
Default header name is `x-openclaw-token`. The feature is off by default — no behavior change for existing deployments.
## Changes
- **`src/config/types.gateway.ts`** — Add `GatewayTokenInjectionConfig` type to `GatewayAuthConfig`
- **`src/config/schema.labels.ts`** / **`src/config/schema.help.ts`** — Add UI labels and help text for the new config fields
- **`src/gateway/control-ui-contract.ts`** — Add optional `token` field to `ControlUiBootstrapConfig`
- **`src/gateway/control-ui.ts`** — Extract token from request header in `handleControlUiHttpRequest()`, include it in the bootstrap JSON config response
- **`ui/src/ui/controllers/control-ui-bootstrap.ts`** — Return token from bootstrap config fetch
- **`ui/src/ui/app-lifecycle.ts`** — When no token is present, await bootstrap config (which may provide a reverse-proxy token) before connecting the WebSocket
## Architecture
Token delivery uses the existing bootstrap JSON config endpoint rather than inline script injection, which is compatible with the Control UI's CSP policy (`script-src 'self'` without `'unsafe-inline'`).
**Token precedence (highest to lowest):**
1. `?token=` query param (client-side, via `applySettingsFromUrl()`) — always wins
2. Existing `localStorage` token — preserved from previous sessions
3. Header injection via bootstrap JSON (this PR) — seeds token when none exists
**Startup flow:**
- If the user already has a token (from localStorage or `?token=`), the WS connects immediately; bootstrap config fetches in the background
- If no token exists, the client awaits the bootstrap config fetch before connecting, so any server-injected token is available for WS auth
## Security Considerations
- Token injection only occurs when explicitly enabled via config (`enabled: true`)
- The header is only read from requests that reach the gateway (behind the reverse proxy)
- Combined with `gateway.trustedProxies`, only requests from trusted IPs are accepted
- Uses `getHeader()` which handles both `string` and `string[]` header values (for proxies that emit duplicate headers)
- No inline scripts — token is delivered via JSON endpoint, respecting the existing CSP policy
- No changes to the WebSocket auth protocol
## Test plan
- [x] Verify existing `?token=` query param flow still works unchanged
- [x] Verify `injectTokenFromHeader.enabled: false` (default) has no effect
- [ ] Verify token injection when `enabled: true` and header is present
- [ ] Verify no injection when header is absent (graceful no-op)
- [ ] Verify header name is case-insensitive (HTTP spec compliance)
- [x] CI passes (node, bun, windows tests all green)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Most Similar PRs
#18273: fix: extract token from URL query string for Control UI websocket auth
by MisterGuy420 · 2026-02-16
73.6%
#23444: Gateway: move auth token storage to state dotenv by default
by bmendonca3 · 2026-02-22
72.4%
#19937: fix(gateway): validate token/password auth modes and isolate gatewa...
by NewdlDewdl · 2026-02-18
71.8%
#19885: test(gateway,browser): isolate tests from ambient OPENCLAW_GATEWAY_...
by NewdlDewdl · 2026-02-18
71.3%
#17746: fix(gateway): add shared-secret fallback to trusted-proxy auth disp...
by dashed · 2026-02-16
71.2%
#16310: fix(ws-connection): skip device pairing when client authenticates w...
by nawinsharma · 2026-02-14
69.9%
#10093: fix: import gateway token from URL param into localStorage
by devjiro76 · 2026-02-06
69.6%
#8121: fix(gateway): remove query parameter token support for hooks
by yubrew · 2026-02-03
69.5%
#22658: Fix onboard ignoring OPENCLAW_GATEWAY_TOKEN env var
by Clawborn · 2026-02-21
69.2%
#23355: Gateway: fail closed on untrusted proxy headers
by bmendonca3 · 2026-02-22
69.1%