← Back to PRs

#10382: feat(gateway): server-side token injection for reverse proxy deployments

by nkuhn-vmw open 2026-02-06 12:17 View on GitHub →
app: web-ui gateway stale size: S
## 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