feat: add token-to-cookie endpoint for embedded chat WebSocket auth (#23280)

## Problem

The VS Code extension embeds the Coder agent chat UI in an iframe,
passing the session token via `postMessage`. HTTP requests use the
`Coder-Session-Token` header, but browser WebSocket connections **cannot
carry custom headers** — they rely on cookies. This causes all WebSocket
requests (e.g. streaming chat messages) to fail with authorization
errors in the embedded iframe.

## Solution

Add `POST /api/v2/users/me/session/token-to-cookie` — a lightweight
endpoint that converts the current (already-validated) session token
into a `Set-Cookie` response. The frontend embed bootstrap flow calls
this immediately after `API.setSessionToken(token)`, before any
WebSocket connections are opened.

### Backend (`coderd/userauth.go`, `coderd/coderd.go`)
- New handler `postSessionTokenCookie` behind `apiKeyMiddleware`.
- Reads the validated token via `httpmw.APITokenFromRequest(r)`.
- Sets an `HttpOnly` cookie with the API key's expiry, applying
site-wide cookie config (Secure, SameSite, host prefix) via
`HTTPCookies.Apply`.
- Returns `204 No Content`.

### Frontend (`site/src/pages/AgentsPage/EmbedContext.tsx`)
- `bootstrapChatEmbedSessionFn` now calls the new endpoint after setting
the header token and before fetching user/permissions.
- The cookie is in place before any WebSocket connections are opened.

## Security

- **No privilege escalation**: The token is already valid — this just
moves it from a header credential to a cookie credential.
- **POST only**: Avoids CSRF-via-navigation.
- **Same origin**: The iframe loads from the Coder server, so the cookie
applies to the correct domain.
- **HttpOnly**: The cookie is not accessible to JavaScript.

> Built with [Coder Agents](https://coder.com/agents) 🤖
This commit is contained in:
Thomas Kosiewski
2026-03-19 16:12:31 +01:00
committed by GitHub
parent c424c31ab8
commit 83809bb380
5 changed files with 89 additions and 0 deletions
@@ -49,7 +49,14 @@ const bootstrapChatEmbedSessionFn = async ({
authorization: AuthorizationRequest;
queryClient: QueryClient;
}) => {
// This is the token forwarded by the VS Code extension's
// ChatPanelProvider via the coder:vscode-auth-bootstrap
// postMessage handshake. See chatPanelProvider.ts in
// coder/vscode-coder for the sending side.
API.setSessionToken(token);
// Exchange the token for a cookie so that WebSocket connections
// (which cannot carry custom headers) are also authenticated.
await API.getAxiosInstance().post("/api/v2/users/me/session/token-to-cookie");
// Fetch user and permissions first, then set them in the cache
// atomically. This avoids a race where invalidating the "me"
// query causes isSignedIn to flip before permissions are ready.