diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 1231da75c7..2ccb23b84c 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7826,6 +7826,29 @@ 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 2313633d51..8d9c7934c0 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6927,6 +6927,27 @@ ] } }, + "/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 84144614e7..0518dbb6d8 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1515,6 +1515,7 @@ 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 6b2aab6c53..980130393b 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -744,6 +744,43 @@ 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/pages/AgentsPage/EmbedContext.tsx b/site/src/pages/AgentsPage/EmbedContext.tsx index e0ae15c819..a49d7f4e4a 100644 --- a/site/src/pages/AgentsPage/EmbedContext.tsx +++ b/site/src/pages/AgentsPage/EmbedContext.tsx @@ -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.