mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
Generated
+23
@@ -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": {
|
"/users/oauth2/github/callback": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|||||||
Generated
+21
@@ -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": {
|
"/users/oauth2/github/callback": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": ["Users"],
|
"tags": ["Users"],
|
||||||
|
|||||||
@@ -1515,6 +1515,7 @@ func New(options *Options) *API {
|
|||||||
r.Post("/", api.postUser)
|
r.Post("/", api.postUser)
|
||||||
r.Get("/", api.users)
|
r.Get("/", api.users)
|
||||||
r.Post("/logout", api.postLogout)
|
r.Post("/logout", api.postLogout)
|
||||||
|
r.Post("/me/session/token-to-cookie", api.postSessionTokenCookie)
|
||||||
r.Get("/oidc-claims", api.userOIDCClaims)
|
r.Get("/oidc-claims", api.userOIDCClaims)
|
||||||
// These routes query information about site wide roles.
|
// These routes query information about site wide roles.
|
||||||
r.Route("/roles", func(r chi.Router) {
|
r.Route("/roles", func(r chi.Router) {
|
||||||
|
|||||||
@@ -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.
|
// GithubOAuth2Team represents a team scoped to an organization.
|
||||||
type GithubOAuth2Team struct {
|
type GithubOAuth2Team struct {
|
||||||
Organization string
|
Organization string
|
||||||
|
|||||||
@@ -49,7 +49,14 @@ const bootstrapChatEmbedSessionFn = async ({
|
|||||||
authorization: AuthorizationRequest;
|
authorization: AuthorizationRequest;
|
||||||
queryClient: QueryClient;
|
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);
|
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
|
// Fetch user and permissions first, then set them in the cache
|
||||||
// atomically. This avoids a race where invalidating the "me"
|
// atomically. This avoids a race where invalidating the "me"
|
||||||
// query causes isSignedIn to flip before permissions are ready.
|
// query causes isSignedIn to flip before permissions are ready.
|
||||||
|
|||||||
Reference in New Issue
Block a user