diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 88acfc1c6f..683fb1a4e1 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7971,29 +7971,6 @@ const docTemplate = `{ ] } }, - "/users/me/session/token-to-cookie": { - "post": { - "description": "Converts the current session token into a Set-Cookie response.\nThis is used by embedded iframes (e.g. VS Code chat) that\nreceive a session token out-of-band via postMessage but need\ncookie-based auth for WebSocket connections.", - "tags": [ - "Authorization" - ], - "summary": "Set session token cookie", - "operationId": "set-session-token-cookie", - "responses": { - "204": { - "description": "No Content" - } - }, - "security": [ - { - "CoderSessionToken": [] - } - ], - "x-apidocgen": { - "skip": true - } - } - }, "/users/oauth2/github/callback": { "get": { "tags": [ diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 27e9509448..d5a74adb09 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7064,27 +7064,6 @@ ] } }, - "/users/me/session/token-to-cookie": { - "post": { - "description": "Converts the current session token into a Set-Cookie response.\nThis is used by embedded iframes (e.g. VS Code chat) that\nreceive a session token out-of-band via postMessage but need\ncookie-based auth for WebSocket connections.", - "tags": ["Authorization"], - "summary": "Set session token cookie", - "operationId": "set-session-token-cookie", - "responses": { - "204": { - "description": "No Content" - } - }, - "security": [ - { - "CoderSessionToken": [] - } - ], - "x-apidocgen": { - "skip": true - } - } - }, "/users/oauth2/github/callback": { "get": { "tags": ["Users"], diff --git a/coderd/coderd.go b/coderd/coderd.go index fba9d2f836..c4330acf11 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1517,7 +1517,6 @@ func New(options *Options) *API { r.Post("/", api.postUser) r.Get("/", api.users) r.Post("/logout", api.postLogout) - r.Post("/me/session/token-to-cookie", api.postSessionTokenCookie) r.Get("/oidc-claims", api.userOIDCClaims) // These routes query information about site wide roles. r.Route("/roles", func(r chi.Router) { diff --git a/coderd/userauth.go b/coderd/userauth.go index 980130393b..6b2aab6c53 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -744,43 +744,6 @@ func (api *API) postLogout(rw http.ResponseWriter, r *http.Request) { }) } -// @Summary Set session token cookie -// @Description Converts the current session token into a Set-Cookie response. -// @Description This is used by embedded iframes (e.g. VS Code chat) that -// @Description receive a session token out-of-band via postMessage but need -// @Description cookie-based auth for WebSocket connections. -// @ID set-session-token-cookie -// @Security CoderSessionToken -// @Tags Authorization -// @Success 204 -// @Router /users/me/session/token-to-cookie [post] -// @x-apidocgen {"skip": true} -func (api *API) postSessionTokenCookie(rw http.ResponseWriter, r *http.Request) { - // Only accept the token from the Coder-Session-Token header. - // Other sources (query params, cookies) should not be allowed - // to bootstrap a new cookie. - token := r.Header.Get(codersdk.SessionTokenHeader) - if token == "" { - httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{ - Message: "Session token must be provided via the Coder-Session-Token header.", - }) - return - } - - apiKey := httpmw.APIKey(r) - - cookie := api.DeploymentValues.HTTPCookies.Apply(&http.Cookie{ - Name: codersdk.SessionTokenCookie, - Value: token, - Path: "/", - HttpOnly: true, - // Expire the cookie when the underlying API key expires. - Expires: apiKey.ExpiresAt, - }) - http.SetCookie(rw, cookie) - rw.WriteHeader(http.StatusNoContent) -} - // GithubOAuth2Team represents a team scoped to an organization. type GithubOAuth2Team struct { Organization string diff --git a/site/src/api/api.ts b/site/src/api/api.ts index bb8e955a92..f80693f070 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -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 => { + const searchParams: Record = {}; + 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; }; diff --git a/site/src/pages/AgentsPage/AgentDetail.tsx b/site/src/pages/AgentsPage/AgentDetail.tsx index 5b744e2e6d..d157a8cbed 100644 --- a/site/src/pages/AgentsPage/AgentDetail.tsx +++ b/site/src/pages/AgentsPage/AgentDetail.tsx @@ -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, }); }, diff --git a/site/src/pages/AgentsPage/components/EmbedContext.tsx b/site/src/pages/AgentsPage/components/EmbedContext.tsx index a49d7f4e4a..f0aaefda98 100644 --- a/site/src/pages/AgentsPage/components/EmbedContext.tsx +++ b/site/src/pages/AgentsPage/components/EmbedContext.tsx @@ -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.