Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix: use unique cookies for workspace proxies#19930

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
deansheather merged 1 commit intomainfromdean/subdomain-cookie
Sep 24, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletionscoderd/coderd.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -785,7 +785,7 @@ func New(options *Options) *API {
options.WorkspaceAppsStatsCollectorOptions.Reporter = api.statsReporter
}

api.workspaceAppServer =&workspaceapps.Server{
api.workspaceAppServer = workspaceapps.NewServer(workspaceapps.ServerOptions{
Logger: workspaceAppsLogger,

DashboardURL: api.AccessURL,
Expand All@@ -799,9 +799,9 @@ func New(options *Options) *API {
StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions),

DisablePathApps: options.DeploymentValues.DisablePathApps.Value(),
Cookies: options.DeploymentValues.HTTPCookies,
CookiesConfig: options.DeploymentValues.HTTPCookies,
APIKeyEncryptionKeycache: options.AppEncryptionKeyCache,
}
})

apiKeyMiddleware := httpmw.ExtractAPIKeyMW(httpmw.ExtractAPIKeyConfig{
DB: options.Database,
Expand Down
6 changes: 5 additions & 1 deletioncoderd/httpapi/cookie.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,11 @@ func StripCoderCookies(header string) string {
name == codersdk.OAuth2StateCookie ||
name == codersdk.OAuth2RedirectCookie ||
name == codersdk.PathAppSessionTokenCookie ||
name == codersdk.SubdomainAppSessionTokenCookie ||
// This uses a prefix check because the subdomain cookie is unique
// per workspace proxy and is based on a hash of the workspace proxy
// subdomain hostname. See the workspaceapps package for more
// details.
strings.HasPrefix(name, codersdk.SubdomainAppSessionTokenCookie) ||
name == codersdk.SignedAppTokenCookie {
continue
}
Expand Down
9 changes: 9 additions & 0 deletionscoderd/httpapi/cookie_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,6 +25,15 @@ func TestStripCoderCookies(t *testing.T) {
}, {
"coder_session_token=ok; oauth_state=wow; oauth_redirect=/",
"",
}, {
"coder_path_app_session_token=ok; wow=test",
"wow=test",
}, {
"coder_subdomain_app_session_token=ok; coder_subdomain_app_session_token_1234567890=ok; wow=test",
"wow=test",
}, {
"coder_signed_app_token=ok; wow=test",
"wow=test",
}} {
t.Run(tc.Input, func(t *testing.T) {
t.Parallel()
Expand Down
66 changes: 16 additions & 50 deletionscoderd/workspaceapps/apptest/apptest.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -264,14 +264,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)

var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)
require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

// Ensure the signed app token cookie is valid.
Expand DownExpand Up@@ -310,14 +303,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)

var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)
require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

// Ensure the signed app token cookie is valid.
Expand DownExpand Up@@ -426,8 +412,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)

appTokenCookie := findCookie(resp.Cookies(), codersdk.SignedAppTokenCookie)
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)
require.Equal(t, appTokenCookie.Path, u.Path, "incorrect path on app token cookie")

object, err := jose.ParseSigned(appTokenCookie.Value, []jose.SignatureAlgorithm{jwtutils.SigningAlgo})
Expand DownExpand Up@@ -467,7 +452,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
assertWorkspaceLastUsedAtUpdated(t, appDetails)

// Since the old token is invalid, the signed app token cookie should have a new value.
newTokenCookie :=findCookie(resp.Cookies(), codersdk.SignedAppTokenCookie)
newTokenCookie :=mustFindCookie(t,resp.Cookies(), codersdk.SignedAppTokenCookie)
require.NotEqual(t, appTokenCookie.Value, newTokenCookie.Value)
})
})
Expand DownExpand Up@@ -978,15 +963,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
resp.Body.Close()
require.Equal(t, http.StatusSeeOther, resp.StatusCode)

cookies := resp.Cookies()
var cookie *http.Cookie
for _, co := range cookies {
if co.Name == c.sessionTokenCookieName {
cookie = co
break
}
}
require.NotNil(t, cookie, "no app session token cookie was set")
cookie := mustFindCookie(t, resp.Cookies(), c.sessionTokenCookieName)
apiKey := cookie.Value

// Fetch the API key from the API.
Expand DownExpand Up@@ -1102,14 +1079,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {

// Parse the returned signed token to verify that it contains the
// prefix.
var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed app token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)

// Parse the JWT without verifying it (since we can't access the key
// from this test).
Expand DownExpand Up@@ -1334,14 +1304,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)

var appTokenCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == codersdk.SignedAppTokenCookie {
appTokenCookie = c
break
}
}
require.NotNil(t, appTokenCookie, "no signed token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)
require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")

// Ensure the signed app token cookie is valid.
Expand DownExpand Up@@ -1589,8 +1552,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
require.Equal(t, proxyTestAppBody, string(body))
require.Equal(t, http.StatusOK, resp.StatusCode)

appTokenCookie := findCookie(resp.Cookies(), codersdk.SignedAppTokenCookie)
require.NotNil(t, appTokenCookie, "no signed token cookie in response")
appTokenCookie := mustFindCookie(t, resp.Cookies(), codersdk.SignedAppTokenCookie)
require.Equal(t, appTokenCookie.Path, "/", "incorrect path on signed token cookie")

object, err := jose.ParseSigned(appTokenCookie.Value, []jose.SignatureAlgorithm{jwtutils.SigningAlgo})
Expand All@@ -1614,7 +1576,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
[]*http.Cookie{
appTokenCookie,
{
Name: codersdk.SubdomainAppSessionTokenCookie,
Name: codersdk.SessionTokenCookie,
Value: apiKey,
},
},
Expand All@@ -1631,7 +1593,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
assertWorkspaceLastUsedAtUpdated(t, appDetails)

// Since the old token is invalid, the signed app token cookie should have a new value.
newTokenCookie :=findCookie(resp.Cookies(), codersdk.SignedAppTokenCookie)
newTokenCookie :=mustFindCookie(t,resp.Cookies(), codersdk.SignedAppTokenCookie)
require.NotEqual(t, appTokenCookie.Value, newTokenCookie.Value)
})
})
Expand DownExpand Up@@ -2542,11 +2504,15 @@ func generateBadJWT(t *testing.T, claims interface{}) string {
return compact
}

func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
func mustFindCookie(t *testing.T, cookies []*http.Cookie, prefix string) *http.Cookie {
t.Helper()
for _, cookie := range cookies {
if cookie.Name == name {
t.Logf("testing cookie against prefix %q: %q", prefix, cookie.Name)
if strings.HasPrefix(cookie.Name, prefix) {
t.Logf("cookie %q found", cookie.Name)
return cookie
}
}
t.Fatalf("cookie with prefix %q not found", prefix)
return nil
}
57 changes: 50 additions & 7 deletionscoderd/workspaceapps/cookies.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,62 @@
package workspaceapps

import (
"crypto/sha256"
"encoding/hex"
"net/http"

"github.com/coder/coder/v2/coderd/httpmw"
"github.com/coder/coder/v2/codersdk"
)

// AppConnectSessionTokenCookieName returns the cookie name for the session
type AppCookies struct {
PathAppSessionToken string
SubdomainAppSessionToken string
SignedAppToken string
}

// NewAppCookies returns the cookie names for the app session token for the
// given hostname. The subdomain cookie is unique per workspace proxy and is
// based on a hash of the workspace proxy subdomain hostname. See
// SubdomainAppSessionTokenCookie for more details.
func NewAppCookies(hostname string) AppCookies {
return AppCookies{
PathAppSessionToken: codersdk.PathAppSessionTokenCookie,
SubdomainAppSessionToken: SubdomainAppSessionTokenCookie(hostname),
SignedAppToken: codersdk.SignedAppTokenCookie,
}
}

// CookieNameForAccessMethod returns the cookie name for the long-lived session
// token for the given access method.
funcAppConnectSessionTokenCookieName(accessMethod AccessMethod) string {
func(c AppCookies) CookieNameForAccessMethod(accessMethod AccessMethod) string {
if accessMethod == AccessMethodSubdomain {
returncodersdk.SubdomainAppSessionTokenCookie
returnc.SubdomainAppSessionToken
}
return codersdk.PathAppSessionTokenCookie
// Path-based and terminal apps are on the same domain:
return c.PathAppSessionToken
}

// SubdomainAppSessionTokenCookie returns the cookie name for the subdomain app
// session token. This is unique per workspace proxy and is based on a hash of
// the workspace proxy subdomain hostname.
//
// The reason the cookie needs to be unique per workspace proxy is to avoid
// cookies from one proxy (e.g. the primary) being sent on requests to a
// different proxy underneath the wildcard.
//
// E.g. `*.dev.coder.com` and `*.sydney.dev.coder.com`
//
// If you have an expired cookie on the primary proxy (valid for
// `*.dev.coder.com`), your browser will send it on all requests to the Sydney
// proxy as it's underneath the wildcard.
//
// By using a unique cookie name per workspace proxy, we can avoid this issue.
func SubdomainAppSessionTokenCookie(hostname string) string {
hash := sha256.Sum256([]byte(hostname))
// 16 bytes of uniqueness is probably enough.
str := hex.EncodeToString(hash[:16])
return codersdk.SubdomainAppSessionTokenCookie + "_" + str
}

// AppConnectSessionTokenFromRequest returns the session token from the request
Expand All@@ -27,22 +70,22 @@ func AppConnectSessionTokenCookieName(accessMethod AccessMethod) string {
// We use different cookie names for:
// - path apps on primary access URL: coder_session_token
// - path apps on proxies: coder_path_app_session_token
// - subdomain apps:coder_subdomain_app_session_token
// - subdomain apps:coder_subdomain_app_session_token_{unique_hash}
//
// First we try the default function to get a token from request, which supports
// query parameters, the Coder-Session-Token header and the coder_session_token
// cookie.
//
// Then we try the specific cookie name for the access method.
funcAppConnectSessionTokenFromRequest(r *http.Request, accessMethod AccessMethod) string {
func(c AppCookies) TokenFromRequest(r *http.Request, accessMethod AccessMethod) string {
// Try the default function first.
token := httpmw.APITokenFromRequest(r)
if token != "" {
return token
}

// Then try the specific cookie name for the access method.
cookie, err := r.Cookie(AppConnectSessionTokenCookieName(accessMethod))
cookie, err := r.Cookie(c.CookieNameForAccessMethod(accessMethod))
if err == nil && cookie.Value != "" {
return cookie.Value
}
Expand Down
34 changes: 34 additions & 0 deletionscoderd/workspaceapps/cookies_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
package workspaceapps_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/workspaceapps"
"github.com/coder/coder/v2/codersdk"
)

func TestAppCookies(t *testing.T) {
t.Parallel()

const (
domain = "example.com"
hash = "a379a6f6eeafb9a55e378c118034e275"
expectedSubdomainCookie = codersdk.SubdomainAppSessionTokenCookie + "_" + hash
)

cookies := workspaceapps.NewAppCookies(domain)
require.Equal(t, codersdk.PathAppSessionTokenCookie, cookies.PathAppSessionToken)
require.Equal(t, expectedSubdomainCookie, cookies.SubdomainAppSessionToken)
require.Equal(t, codersdk.SignedAppTokenCookie, cookies.SignedAppToken)

require.Equal(t, cookies.PathAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodPath))
require.Equal(t, cookies.PathAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodTerminal))
require.Equal(t, cookies.SubdomainAppSessionToken, cookies.CookieNameForAccessMethod(workspaceapps.AccessMethodSubdomain))

// A new cookies object with a different domain should have a different
// subdomain cookie.
newCookies := workspaceapps.NewAppCookies("different.com")
require.NotEqual(t, cookies.SubdomainAppSessionToken, newCookies.SubdomainAppSessionToken)
}
9 changes: 9 additions & 0 deletionscoderd/workspaceapps/db_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -1236,6 +1236,15 @@ func workspaceappsResolveRequest(t testing.TB, connLogger connectionlog.Connecti
if opts.SignedTokenProvider != nil && connLogger != nil {
opts.SignedTokenProvider = signedTokenProviderWithConnLogger(t, opts.SignedTokenProvider, connLogger, time.Hour)
}
if opts.Cookies.PathAppSessionToken == "" {
opts.Cookies.PathAppSessionToken = codersdk.PathAppSessionTokenCookie
}
if opts.Cookies.SubdomainAppSessionToken == "" {
opts.Cookies.SubdomainAppSessionToken = codersdk.SubdomainAppSessionTokenCookie + "_test"
}
if opts.Cookies.SignedAppToken == "" {
opts.Cookies.SignedAppToken = codersdk.SignedAppTokenCookie
}

tracing.StatusWriterMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpmw.AttachRequestID(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
3 changes: 2 additions & 1 deletioncoderd/workspaceapps/provider.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -22,6 +22,7 @@ const (
type ResolveRequestOptions struct {
Logger slog.Logger
SignedTokenProvider SignedTokenProvider
Cookies AppCookies
CookieCfg codersdk.HTTPCookieConfig

DashboardURL *url.URL
Expand DownExpand Up@@ -58,7 +59,7 @@ func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequest
AppRequest: appReq,
PathAppBaseURL: opts.PathAppBaseURL.String(),
AppHostname: opts.AppHostname,
SessionToken:AppConnectSessionTokenFromRequest(r, appReq.AccessMethod),
SessionToken:opts.Cookies.TokenFromRequest(r, appReq.AccessMethod),
AppPath: opts.AppPath,
AppQuery: opts.AppQuery,
}
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp