Compare commits

...

2 Commits

Author SHA1 Message Date
Ethan acb3aa8918 fix(codersdk): use header auth for non-browser websocket dials (#22461)
## Context
This commit is part of the fix for a downstream provider outage observed
during
`coderd_template` updates.

Observed downstream symptoms (terraform-provider-coderd):
- Template-version websocket log stream requests returned `401`:
  `GET /api/v2/templateversions/<id>/logs`.
- In older provider code (`waitForJob`), stream-init errors could
produce
`(nil, nil, err)` and then trigger a nil dereference when
`closer.Close()`
  was deferred before checking `err`.
- Net effect: template update path crashed instead of returning a
controlled
  provisioning error.

That provider panic is being hardened in the provider repo separately
(https://github.com/coder/terraform-provider-coderd/pull/308). This
commit addresses the upstream SDK auth mismatch that caused the
websocket `401`
side of the chain.

## Root cause

On deployments with host-prefixed cookie handling (dev.coder.com)
enabled
(`--host-prefix-cookie` / `EnableHostPrefix=true`), middleware rewrites
cookie
state to enforce prefixed auth cookies.

For non-browser websocket clients that still sent unprefixed
`coder_session_token` via cookie jars, this created an auth mismatch:
- cookie-based credential expected by the client path,
- but cookie normalization/stripping applied server-side,
- resulting in no usable token at auth extraction time.

## Fix in this commit

Apply the #22226 non-browser auth principle to remaining websocket
callsites in
`codersdk` by replacing cookie-jar session auth with header-token auth.

_Generated with mux but reviewed by a human_
2026-03-02 08:37:39 +00:00
Steven Masley c339aa99ee chore: use header auth over cookies for agents (#22226) (cherry-pick/v2.31) (#22313)
All non-browser connections should not use cookies

(cherry picked from commit 3353e687e7)
2026-02-26 11:11:00 -06:00
4 changed files with 30 additions and 79 deletions
+4 -19
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"sync"
"time"
@@ -321,21 +320,15 @@ func (c *Client) connectRPCVersion(ctx context.Context, version *apiversion.APIV
}
rpcURL.RawQuery = q.Encode()
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(rpcURL, []*http.Cookie{{
Name: codersdk.SessionTokenCookie,
Value: c.SDK.SessionToken(),
}})
httpClient := &http.Client{
Jar: jar,
Transport: c.SDK.HTTPClient.Transport,
}
// nolint:bodyclose
conn, res, err := websocket.Dial(ctx, rpcURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
HTTPHeader: http.Header{
codersdk.SessionTokenHeader: []string{c.SDK.SessionToken()},
},
})
if err != nil {
if res == nil {
@@ -709,16 +702,7 @@ func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, err
return nil, xerrors.Errorf("parse url: %w", err)
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(rpcURL, []*http.Cookie{{
Name: codersdk.SessionTokenCookie,
Value: c.SDK.SessionToken(),
}})
httpClient := &http.Client{
Jar: jar,
Transport: c.SDK.HTTPClient.Transport,
}
@@ -726,6 +710,7 @@ func (c *Client) WaitForReinit(ctx context.Context) (*ReinitializationEvent, err
if err != nil {
return nil, xerrors.Errorf("build request: %w", err)
}
req.Header[codersdk.SessionTokenHeader] = []string{c.SDK.SessionToken()}
res, err := httpClient.Do(req)
if err != nil {
+6 -21
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"slices"
"strings"
"time"
@@ -239,20 +238,14 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after
if err != nil {
return nil, nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(followURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
httpClient := &http.Client{
Jar: jar,
Transport: c.HTTPClient.Transport,
}
conn, res, err := websocket.Dial(ctx, followURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
HTTPClient: httpClient,
HTTPHeader: http.Header{
SessionTokenHeader: []string{c.SessionToken()},
},
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
@@ -325,16 +318,8 @@ func (c *Client) ServeProvisionerDaemon(ctx context.Context, req ServeProvisione
headers.Set(ProvisionerDaemonPSK, req.PreSharedKey)
}
if req.ProvisionerKey == "" && req.PreSharedKey == "" {
// use session token if we don't have a PSK or provisioner key.
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
httpClient.Jar = jar
// Use session token if we don't have a PSK or provisioner key.
headers.Set(SessionTokenHeader, c.SessionToken())
}
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
+7 -22
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"strings"
"time"
@@ -580,24 +579,16 @@ func (c *Client) WatchWorkspaceAgentContainers(ctx context.Context, agentID uuid
return nil, nil, err
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(reqURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
conn, res, err := websocket.Dial(ctx, reqURL.String(), &websocket.DialOptions{
// We want `NoContextTakeover` compression to balance improving
// bandwidth cost/latency with minimal memory usage overhead.
CompressionMode: websocket.CompressionNoContextTakeover,
HTTPClient: &http.Client{
Jar: jar,
Transport: c.HTTPClient.Transport,
},
HTTPHeader: http.Header{
SessionTokenHeader: []string{c.SessionToken()},
},
})
if err != nil {
if res == nil {
@@ -687,20 +678,14 @@ func (c *Client) WorkspaceAgentLogsAfter(ctx context.Context, agentID uuid.UUID,
return ch, closeFunc(func() error { return nil }), nil
}
jar, err := cookiejar.New(nil)
if err != nil {
return nil, nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(reqURL, []*http.Cookie{{
Name: SessionTokenCookie,
Value: c.SessionToken(),
}})
httpClient := &http.Client{
Jar: jar,
Transport: c.HTTPClient.Transport,
}
conn, res, err := websocket.Dial(ctx, reqURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
HTTPClient: httpClient,
HTTPHeader: http.Header{
SessionTokenHeader: []string{c.SessionToken()},
},
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
+13 -17
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"net"
"net/http"
"net/http/cookiejar"
"net/netip"
"os"
"strconv"
@@ -363,26 +362,23 @@ func (c *Client) AgentReconnectingPTY(ctx context.Context, opts WorkspaceAgentRe
}
serverURL.RawQuery = q.Encode()
// If we're not using a signed token, we need to set the session token as a
// cookie.
httpClient := c.client.HTTPClient
// Shallow-clone the HTTP client so we never inherit a caller-provided
// cookie jar. Non-browser websocket auth uses the Coder-Session-Token
// header or a signed-token query param — never cookies. A stale jar
// cookie would take precedence on the server (cookies are checked
// before headers) and cause spurious 401s.
wsHTTPClient := *c.client.HTTPClient
wsHTTPClient.Jar = nil
headers := http.Header{}
// If we're not using a signed token, set the session token header.
if opts.SignedToken == "" {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, xerrors.Errorf("create cookie jar: %w", err)
}
jar.SetCookies(serverURL, []*http.Cookie{{
Name: codersdk.SessionTokenCookie,
Value: c.client.SessionToken(),
}})
httpClient = &http.Client{
Jar: jar,
Transport: c.client.HTTPClient.Transport,
}
headers.Set(codersdk.SessionTokenHeader, c.client.SessionToken())
}
//nolint:bodyclose
conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{
HTTPClient: httpClient,
HTTPClient: &wsHTTPClient,
HTTPHeader: headers,
})
if err != nil {
if res == nil {