mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: pass session token as query param on agent chat WebSockets (#23405)
## Problem When the Coder chat UI is embedded in a VS Code webview, the session token is set via the Coder-Session-Token header for HTTP requests. However, browsers cannot attach custom headers to WebSocket connections, and VS Code Electron webview environment does not support cookies set via Set-Cookie from iframe origins. This causes all chat WebSocket connections to fail with authorization errors. ## Solution Pass the session token as a coder_session_token query parameter on all chat-related WebSocket connections. The backend already accepts this parameter (see APITokenFromRequest in coderd/httpmw/apikey.go). The token is only included when API.getSessionToken() returns a value, which only happens in the embed bootstrap flow. Normal browser sessions use cookies and are unaffected. > Built with [Coder Agents](https://coder.com/agents)
This commit is contained in:
@@ -147,6 +147,10 @@ export const watchChat = (
|
||||
if (afterMessageId !== undefined && afterMessageId > 0) {
|
||||
params.set("after_id", afterMessageId.toString());
|
||||
}
|
||||
const token = API.getSessionToken();
|
||||
if (token) {
|
||||
params.set(SessionTokenCookie, token);
|
||||
}
|
||||
const query = params.toString();
|
||||
const route = `/api/experimental/chats/${chatId}/stream${query ? `?${query}` : ""}`;
|
||||
return new OneWayWebSocket({
|
||||
@@ -155,8 +159,14 @@ export const watchChat = (
|
||||
};
|
||||
|
||||
export const watchChats = (): OneWayWebSocket<TypesGen.ServerSentEvent> => {
|
||||
const searchParams: Record<string, string> = {};
|
||||
const token = API.getSessionToken();
|
||||
if (token) {
|
||||
searchParams[SessionTokenCookie] = token;
|
||||
}
|
||||
return new OneWayWebSocket({
|
||||
apiRoute: "/api/experimental/chats/watch",
|
||||
searchParams,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3473,6 +3483,14 @@ function createWebSocket(
|
||||
path: string,
|
||||
params: URLSearchParams = new URLSearchParams(),
|
||||
) {
|
||||
// When running in an embedded context (e.g. VS Code webview),
|
||||
// the session token is set via the API header but browsers
|
||||
// cannot attach custom headers to WebSocket connections.
|
||||
// Pass it as a query parameter instead.
|
||||
const token = API.getSessionToken();
|
||||
if (token) {
|
||||
params.set(SessionTokenCookie, token);
|
||||
}
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const socket = new WebSocket(
|
||||
`${protocol}//${location.host}${path}?${params}`,
|
||||
@@ -3485,6 +3503,7 @@ function createWebSocket(
|
||||
interface ClientApi extends ApiMethods {
|
||||
getCsrfToken: () => string;
|
||||
setSessionToken: (token: string) => void;
|
||||
getSessionToken: () => string | undefined;
|
||||
setHost: (host: string | undefined) => void;
|
||||
getAxiosInstance: () => AxiosInstance;
|
||||
}
|
||||
@@ -3508,6 +3527,12 @@ export class Api extends ApiMethods implements ClientApi {
|
||||
this.axios.defaults.headers.common["Coder-Session-Token"] = token;
|
||||
};
|
||||
|
||||
getSessionToken = (): string | undefined => {
|
||||
return this.axios.defaults.headers.common["Coder-Session-Token"] as
|
||||
| string
|
||||
| undefined;
|
||||
};
|
||||
|
||||
setHost = (host: string | undefined): void => {
|
||||
this.axios.defaults.baseURL = host;
|
||||
};
|
||||
|
||||
@@ -748,6 +748,12 @@ const AgentDetail: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the active git repo root so VS Code opens to the
|
||||
// actual project directory, falling back to the agent's
|
||||
// configured directory.
|
||||
const repoRoots = Array.from(gitWatcher.repositories.keys()).sort();
|
||||
const folder = repoRoots[0] ?? workspaceAgent.expanded_directory;
|
||||
|
||||
generateKeyMutation.mutate(undefined, {
|
||||
onSuccess: ({ key }) => {
|
||||
location.href = getVSCodeHref(editor, {
|
||||
@@ -755,7 +761,7 @@ const AgentDetail: FC = () => {
|
||||
workspace: workspace.name,
|
||||
token: key,
|
||||
agent: workspaceAgent.name,
|
||||
folder: workspaceAgent.expanded_directory,
|
||||
folder,
|
||||
chatId: agentId,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -54,9 +54,6 @@ const bootstrapChatEmbedSessionFn = async ({
|
||||
// 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.
|
||||
|
||||
Reference in New Issue
Block a user