From 9de7ee33a580603b00f48b376c2ae18b1e450026 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 11 Mar 2025 15:40:14 -0500 Subject: [PATCH 01/11] chore: add custom samesite options to auth cookies Advanced feature, not recommended to use --- cli/server.go | 2 +- coderd/apikey.go | 6 ++--- coderd/coderd.go | 6 ++--- coderd/httpmw/authz.go | 15 ++++++++++++ coderd/httpmw/csrf.go | 4 ++-- coderd/httpmw/oauth2.go | 12 ++++------ coderd/userauth.go | 4 ++-- coderd/workspaceapps/provider.go | 5 ++-- coderd/workspaceapps/proxy.go | 13 ++++++----- codersdk/deployment.go | 40 ++++++++++++++++++++++++++++++-- 10 files changed, 78 insertions(+), 29 deletions(-) diff --git a/cli/server.go b/cli/server.go index ea6f4d665f4de..af3792a069c27 100644 --- a/cli/server.go +++ b/cli/server.go @@ -641,7 +641,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. GoogleTokenValidator: googleTokenValidator, ExternalAuthConfigs: externalAuthConfigs, RealIPConfig: realIPConfig, - SecureAuthCookie: vals.SecureAuthCookie.Value(), + Cookies: vals.HTTPCookies, SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), diff --git a/coderd/apikey.go b/coderd/apikey.go index becb9737ed62e..2ad0c30fb45c5 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -382,12 +382,10 @@ func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (* APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(newkey)}, }) - return &http.Cookie{ + return api.Cookies.Apply(&http.Cookie{ Name: codersdk.SessionTokenCookie, Value: sessionToken, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: api.SecureAuthCookie, - }, &newkey, nil + }), &newkey, nil } diff --git a/coderd/coderd.go b/coderd/coderd.go index 1eefd15a8d655..f6a644aa06612 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -155,7 +155,7 @@ type Options struct { GithubOAuth2Config *GithubOAuth2Config OIDCConfig *OIDCConfig PrometheusRegistry *prometheus.Registry - SecureAuthCookie bool + Cookies codersdk.HTTPCookieConfig StrictTransportSecurityCfg httpmw.HSTSConfig SSHKeygenAlgorithm gitsshkey.Algorithm Telemetry telemetry.Reporter @@ -740,7 +740,7 @@ func New(options *Options) *API { StatsCollector: workspaceapps.NewStatsCollector(options.WorkspaceAppsStatsCollectorOptions), DisablePathApps: options.DeploymentValues.DisablePathApps.Value(), - SecureAuthCookie: options.DeploymentValues.SecureAuthCookie.Value(), + Cookies: options.DeploymentValues.HTTPCookies, APIKeyEncryptionKeycache: options.AppEncryptionKeyCache, } @@ -828,7 +828,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - httpmw.CSRF(options.SecureAuthCookie), + httpmw.CSRF(options.Cookies), ) // This incurs a performance hit from the middleware, but is required to make sure diff --git a/coderd/httpmw/authz.go b/coderd/httpmw/authz.go index 4c94ce362be2a..521ccf949c063 100644 --- a/coderd/httpmw/authz.go +++ b/coderd/httpmw/authz.go @@ -35,3 +35,18 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht }) } } +<<<<<<< Updated upstream +======= + +// RecordAuthzChecks enables recording all the authorization checks that +// occurred in the processing of a request. This is mostly helpful for debugging +// and understanding what permissions are required for a given action without +// needing to go hunting for checks in the code, where you're quite likely to +// miss something subtle or a check happening somewhere you didn't expect. +func RecordAuthzChecks(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + r = r.WithContext(rbac.WithAuthzCheckRecorder(r.Context())) + next.ServeHTTP(rw, r) + }) +} +>>>>>>> Stashed changes diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go index 8cd043146c082..41e9f87855055 100644 --- a/coderd/httpmw/csrf.go +++ b/coderd/httpmw/csrf.go @@ -16,10 +16,10 @@ import ( // for non-GET requests. // If enforce is false, then CSRF enforcement is disabled. We still want // to include the CSRF middleware because it will set the CSRF cookie. -func CSRF(secureCookie bool) func(next http.Handler) http.Handler { +func CSRF(cookieCfg codersdk.HTTPCookieConfig) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { mw := nosurf.New(next) - mw.SetBaseCookie(http.Cookie{Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: secureCookie}) + mw.SetBaseCookie(*cookieCfg.Apply(&http.Cookie{Path: "/", HttpOnly: true})) mw.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sessCookie, err := r.Cookie(codersdk.SessionTokenCookie) if err == nil && diff --git a/coderd/httpmw/oauth2.go b/coderd/httpmw/oauth2.go index 49e98da685e0f..25bf80e934d98 100644 --- a/coderd/httpmw/oauth2.go +++ b/coderd/httpmw/oauth2.go @@ -40,7 +40,7 @@ func OAuth2(r *http.Request) OAuth2State { // a "code" URL parameter will be redirected. // AuthURLOpts are passed to the AuthCodeURL function. If this is nil, // the default option oauth2.AccessTypeOffline will be used. -func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOpts map[string]string) func(http.Handler) http.Handler { +func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, cookieCfg codersdk.HTTPCookieConfig, authURLOpts map[string]string) func(http.Handler) http.Handler { opts := make([]oauth2.AuthCodeOption, 0, len(authURLOpts)+1) opts = append(opts, oauth2.AccessTypeOffline) for k, v := range authURLOpts { @@ -118,22 +118,20 @@ func ExtractOAuth2(config promoauth.OAuth2Config, client *http.Client, authURLOp } } - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, cookieCfg.Apply(&http.Cookie{ Name: codersdk.OAuth2StateCookie, Value: state, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) + })) // Redirect must always be specified, otherwise // an old redirect could apply! - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, cookieCfg.Apply(&http.Cookie{ Name: codersdk.OAuth2RedirectCookie, Value: redirect, Path: "/", HttpOnly: true, - SameSite: http.SameSiteLaxMode, - }) + })) http.Redirect(rw, r, config.AuthCodeURL(state, opts...), http.StatusTemporaryRedirect) return diff --git a/coderd/userauth.go b/coderd/userauth.go index abbe2b4a9f2eb..18f1f74a3c66e 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -204,7 +204,7 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) { Path: "/", Value: token, Expires: claims.Expiry.Time(), - Secure: api.SecureAuthCookie, + Secure: api.Cookies.Secure.Value(), HttpOnly: true, // Must be SameSite to work on the redirected auth flow from the // oauth provider. @@ -1917,7 +1917,7 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C Name: codersdk.SessionTokenCookie, Path: "/", MaxAge: -1, - Secure: api.SecureAuthCookie, + Secure: api.Cookies.Secure.Value(), HttpOnly: true, }) // This is intentional setting the key to the deleted old key, diff --git a/coderd/workspaceapps/provider.go b/coderd/workspaceapps/provider.go index 1887036e35cbf..1cd652976f6f4 100644 --- a/coderd/workspaceapps/provider.go +++ b/coderd/workspaceapps/provider.go @@ -22,6 +22,7 @@ const ( type ResolveRequestOptions struct { Logger slog.Logger SignedTokenProvider SignedTokenProvider + CookieCfg codersdk.HTTPCookieConfig DashboardURL *url.URL PathAppBaseURL *url.URL @@ -75,12 +76,12 @@ func ResolveRequest(rw http.ResponseWriter, r *http.Request, opts ResolveRequest // // For subdomain apps, this applies to the entire subdomain, e.g. // app--agent--workspace--user.apps.example.com - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, opts.CookieCfg.Apply(&http.Cookie{ Name: codersdk.SignedAppTokenCookie, Value: tokenStr, Path: appReq.BasePath, Expires: token.Expiry.Time(), - }) + })) return token, true } diff --git a/coderd/workspaceapps/proxy.go b/coderd/workspaceapps/proxy.go index de97f6197a28c..bc8d32ed2ead9 100644 --- a/coderd/workspaceapps/proxy.go +++ b/coderd/workspaceapps/proxy.go @@ -110,8 +110,8 @@ type Server struct { // // Subdomain apps are safer with their cookies scoped to the subdomain, and XSS // calls to the dashboard are not possible due to CORs. - DisablePathApps bool - SecureAuthCookie bool + DisablePathApps bool + Cookies codersdk.HTTPCookieConfig AgentProvider AgentProvider StatsCollector *StatsCollector @@ -230,16 +230,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request, // We use different cookie names for path apps and for subdomain apps to // avoid both being set and sent to the server at the same time and the // server using the wrong value. - http.SetCookie(rw, &http.Cookie{ + http.SetCookie(rw, s.Cookies.Apply(&http.Cookie{ Name: AppConnectSessionTokenCookieName(accessMethod), Value: payload.APIKey, Domain: domain, Path: "/", MaxAge: 0, HttpOnly: true, - SameSite: http.SameSiteLaxMode, - Secure: s.SecureAuthCookie, - }) + })) // Strip the query parameter. path := r.URL.Path @@ -300,6 +298,7 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) // permissions to connect to a workspace. token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, @@ -405,6 +404,7 @@ func (s *Server) HandleSubdomain(middlewares ...func(http.Handler) http.Handler) token, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, @@ -630,6 +630,7 @@ func (s *Server) workspaceAgentPTY(rw http.ResponseWriter, r *http.Request) { appToken, ok := ResolveRequest(rw, r, ResolveRequestOptions{ Logger: s.Logger, + CookieCfg: s.Cookies, SignedTokenProvider: s.SignedTokenProvider, DashboardURL: s.DashboardURL, PathAppBaseURL: s.AccessURL, diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 089bd11567ab7..730f8663a7b26 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -358,7 +358,7 @@ type DeploymentValues struct { Telemetry TelemetryConfig `json:"telemetry,omitempty" typescript:",notnull"` TLS TLSConfig `json:"tls,omitempty" typescript:",notnull"` Trace TraceConfig `json:"trace,omitempty" typescript:",notnull"` - SecureAuthCookie serpent.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` + HTTPCookies HTTPCookieConfig `json:"http_cookies,omitempty" typescript:",notnull"` StrictTransportSecurity serpent.Int64 `json:"strict_transport_security,omitempty" typescript:",notnull"` StrictTransportSecurityOptions serpent.StringArray `json:"strict_transport_security_options,omitempty" typescript:",notnull"` SSHKeygenAlgorithm serpent.String `json:"ssh_keygen_algorithm,omitempty" typescript:",notnull"` @@ -586,6 +586,30 @@ type TraceConfig struct { DataDog serpent.Bool `json:"data_dog" typescript:",notnull"` } +type HTTPCookieConfig struct { + Secure serpent.Bool `json:"secure_auth_cookie,omitempty" typescript:",notnull"` + SameSite string `json:"same_site,omitempty" typescript:",notnull"` +} + +func (cfg HTTPCookieConfig) Apply(c *http.Cookie) *http.Cookie { + c.Secure = cfg.Secure.Value() + c.SameSite = cfg.HTTPSameSite() + return c +} + +func (cfg HTTPCookieConfig) HTTPSameSite() http.SameSite { + switch strings.ToLower(cfg.SameSite) { + case "lax": + return http.SameSiteLaxMode + case "strict": + return http.SameSiteStrictMode + case "none": + return http.SameSiteNoneMode + default: + return http.SameSiteDefaultMode + } +} + type ExternalAuthConfig struct { // Type is the type of external auth config. Type string `json:"type" yaml:"type"` @@ -2376,11 +2400,23 @@ func (c *DeploymentValues) Options() serpent.OptionSet { Description: "Controls if the 'Secure' property is set on browser session cookies.", Flag: "secure-auth-cookie", Env: "CODER_SECURE_AUTH_COOKIE", - Value: &c.SecureAuthCookie, + Value: &c.HTTPCookies.Secure, Group: &deploymentGroupNetworking, YAML: "secureAuthCookie", Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"), }, + { + Name: "SameSite Auth Cookie", + Description: "Controls the 'SameSite' property is set on browser session cookies.", + Flag: "samesite-auth-cookie", + Env: "CODER_SAMESITE_AUTH_COOKIE", + // Do not allow "strict" same-site cookies. That would potentially break workspace apps. + Value: serpent.EnumOf(&c.HTTPCookies.SameSite, "lax", "none"), + Default: "lax", + Group: &deploymentGroupNetworking, + YAML: "sameSiteAuthCookie", + Annotations: serpent.Annotations{}.Mark(annotationExternalProxies, "true"), + }, { Name: "Terms of Service URL", Description: "A URL to an external Terms of Service that must be accepted by users when logging in.", From 4f6e753c74913eb2196d21909ecda17a779ccd18 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 12 Mar 2025 09:10:02 -0500 Subject: [PATCH 02/11] fixup! chore: add custom samesite options to auth cookies --- coderd/httpmw/authz.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/coderd/httpmw/authz.go b/coderd/httpmw/authz.go index 521ccf949c063..4c94ce362be2a 100644 --- a/coderd/httpmw/authz.go +++ b/coderd/httpmw/authz.go @@ -35,18 +35,3 @@ func AsAuthzSystem(mws ...func(http.Handler) http.Handler) func(http.Handler) ht }) } } -<<<<<<< Updated upstream -======= - -// RecordAuthzChecks enables recording all the authorization checks that -// occurred in the processing of a request. This is mostly helpful for debugging -// and understanding what permissions are required for a given action without -// needing to go hunting for checks in the code, where you're quite likely to -// miss something subtle or a check happening somewhere you didn't expect. -func RecordAuthzChecks(next http.Handler) http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - r = r.WithContext(rbac.WithAuthzCheckRecorder(r.Context())) - next.ServeHTTP(rw, r) - }) -} ->>>>>>> Stashed changes From 8a3b82020f93499c006c658c3bf12b1bc786f7a4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 09:42:23 -0500 Subject: [PATCH 03/11] chore: fixup tests --- coderd/coderd.go | 6 +++--- coderd/httpmw/csrf_test.go | 4 ++-- coderd/httpmw/oauth2_test.go | 23 ++++++++++++++--------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index f6a644aa06612..758336db5ced9 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -868,7 +868,7 @@ func New(options *Options) *API { r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) { r.Use( apiKeyMiddlewareRedirect, - httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, nil), + httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, codersdk.HTTPCookieConfig{}, nil), ) r.Get("/", api.externalAuthCallback(externalAuthConfig)) }) @@ -1123,14 +1123,14 @@ func New(options *Options) *API { r.Get("/github/device", api.userOAuth2GithubDevice) r.Route("/github", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, nil), + httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, codersdk.HTTPCookieConfig{}, nil), ) r.Get("/callback", api.userOAuth2Github) }) }) r.Route("/oidc/callback", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, oidcAuthURLParams), + httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, codersdk.HTTPCookieConfig{}, oidcAuthURLParams), ) r.Get("/", api.userOIDC) }) diff --git a/coderd/httpmw/csrf_test.go b/coderd/httpmw/csrf_test.go index 03f2babb2961a..9e8094ad50d6d 100644 --- a/coderd/httpmw/csrf_test.go +++ b/coderd/httpmw/csrf_test.go @@ -53,7 +53,7 @@ func TestCSRFExemptList(t *testing.T) { }, } - mw := httpmw.CSRF(false) + mw := httpmw.CSRF(codersdk.HTTPCookieConfig{}) csrfmw := mw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})).(*nosurf.CSRFHandler) for _, c := range cases { @@ -87,7 +87,7 @@ func TestCSRFError(t *testing.T) { var handler http.Handler = http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusOK) }) - handler = httpmw.CSRF(false)(handler) + handler = httpmw.CSRF(codersdk.HTTPCookieConfig{})(handler) // Not testing the error case, just providing the example of things working // to base the failure tests off of. diff --git a/coderd/httpmw/oauth2_test.go b/coderd/httpmw/oauth2_test.go index ca5dcf5f8a52d..9739735f3eaf7 100644 --- a/coderd/httpmw/oauth2_test.go +++ b/coderd/httpmw/oauth2_test.go @@ -50,7 +50,7 @@ func TestOAuth2(t *testing.T) { t.Parallel() req := httptest.NewRequest("GET", "/", nil) res := httptest.NewRecorder() - httpmw.ExtractOAuth2(nil, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(nil, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusBadRequest, res.Result().StatusCode) }) t.Run("RedirectWithoutCode", func(t *testing.T) { @@ -58,7 +58,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape("/dashboard"), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") if !assert.NotEmpty(t, location) { return @@ -82,7 +82,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?redirect="+url.QueryEscape(uri.String()), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") if !assert.NotEmpty(t, location) { return @@ -97,7 +97,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?code=something", nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusBadRequest, res.Result().StatusCode) }) t.Run("NoStateCookie", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?code=something&state=test", nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) }) t.Run("MismatchedState", func(t *testing.T) { @@ -117,7 +117,7 @@ func TestOAuth2(t *testing.T) { }) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(nil).ServeHTTP(res, req) require.Equal(t, http.StatusUnauthorized, res.Result().StatusCode) }) t.Run("ExchangeCodeAndState", func(t *testing.T) { @@ -133,7 +133,7 @@ func TestOAuth2(t *testing.T) { }) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, nil)(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { state := httpmw.OAuth2(r) require.Equal(t, "/dashboard", state.Redirect) })).ServeHTTP(res, req) @@ -144,7 +144,7 @@ func TestOAuth2(t *testing.T) { res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("foo", "bar")) authOpts := map[string]string{"foo": "bar"} - httpmw.ExtractOAuth2(tp, nil, authOpts)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{}, authOpts)(nil).ServeHTTP(res, req) location := res.Header().Get("Location") // Ideally we would also assert that the location contains the query params // we set in the auth URL but this would essentially be testing the oauth2 package. @@ -157,12 +157,17 @@ func TestOAuth2(t *testing.T) { req := httptest.NewRequest("GET", "/?oidc_merge_state="+customState+"&redirect="+url.QueryEscape("/dashboard"), nil) res := httptest.NewRecorder() tp := newTestOAuth2Provider(t, oauth2.AccessTypeOffline) - httpmw.ExtractOAuth2(tp, nil, nil)(nil).ServeHTTP(res, req) + httpmw.ExtractOAuth2(tp, nil, codersdk.HTTPCookieConfig{ + Secure: true, + SameSite: "none", + }, nil)(nil).ServeHTTP(res, req) found := false for _, cookie := range res.Result().Cookies() { if cookie.Name == codersdk.OAuth2StateCookie { require.Equal(t, cookie.Value, customState, "expected state") + require.Equal(t, true, cookie.Secure, "cookie set to secure") + require.Equal(t, http.SameSiteNoneMode, cookie.SameSite, "same-site = none") found = true } } From 77da925a3803986bab7672663d5fb822a9d81613 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 09:59:23 -0500 Subject: [PATCH 04/11] make gen --- enterprise/cli/proxyserver.go | 2 +- enterprise/wsproxy/wsproxy.go | 8 ++++---- site/src/api/typesGenerated.ts | 8 +++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/enterprise/cli/proxyserver.go b/enterprise/cli/proxyserver.go index ec77936accd12..35f0986614840 100644 --- a/enterprise/cli/proxyserver.go +++ b/enterprise/cli/proxyserver.go @@ -264,7 +264,7 @@ func (r *RootCmd) proxyServer() *serpent.Command { Tracing: tracer, PrometheusRegistry: prometheusRegistry, APIRateLimit: int(cfg.RateLimit.API.Value()), - SecureAuthCookie: cfg.SecureAuthCookie.Value(), + CookieConfig: cfg.HTTPCookies, DisablePathApps: cfg.DisablePathApps.Value(), ProxySessionToken: proxySessionToken.Value(), AllowAllCors: cfg.Dangerous.AllowAllCors.Value(), diff --git a/enterprise/wsproxy/wsproxy.go b/enterprise/wsproxy/wsproxy.go index 9108283513e4f..5dbf8ab6ea24d 100644 --- a/enterprise/wsproxy/wsproxy.go +++ b/enterprise/wsproxy/wsproxy.go @@ -70,7 +70,7 @@ type Options struct { TLSCertificates []tls.Certificate APIRateLimit int - SecureAuthCookie bool + CookieConfig codersdk.HTTPCookieConfig DisablePathApps bool DERPEnabled bool DERPServerRelayAddress string @@ -310,8 +310,8 @@ func New(ctx context.Context, opts *Options) (*Server, error) { Logger: s.Logger.Named("proxy_token_provider"), }, - DisablePathApps: opts.DisablePathApps, - SecureAuthCookie: opts.SecureAuthCookie, + DisablePathApps: opts.DisablePathApps, + Cookies: opts.CookieConfig, AgentProvider: agentProvider, StatsCollector: workspaceapps.NewStatsCollector(opts.StatsCollectorOptions), @@ -362,7 +362,7 @@ func New(ctx context.Context, opts *Options) (*Server, error) { }, // CSRF is required here because we need to set the CSRF cookies on // responses. - httpmw.CSRF(s.Options.SecureAuthCookie), + httpmw.CSRF(s.Options.CookieConfig), ) // Attach workspace apps routes. diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 0fd31361e69a3..09da288ceeb76 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -649,7 +649,7 @@ export interface DeploymentValues { readonly telemetry?: TelemetryConfig; readonly tls?: TLSConfig; readonly trace?: TraceConfig; - readonly secure_auth_cookie?: boolean; + readonly http_cookies?: HTTPCookieConfig; readonly strict_transport_security?: number; readonly strict_transport_security_options?: string; readonly ssh_keygen_algorithm?: string; @@ -976,6 +976,12 @@ export interface GroupSyncSettings { readonly legacy_group_name_mapping?: Record; } +// From codersdk/deployment.go +export interface HTTPCookieConfig { + readonly secure_auth_cookie?: boolean; + readonly same_site?: string; +} + // From health/model.go export type HealthCode = | "EACS03" From 71965d82846c64c18d7ee061a501d9d8d76746a7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 24 Mar 2025 21:08:51 -0500 Subject: [PATCH 05/11] fixup test with incorrect reference --- codersdk/deployment.go | 2 +- enterprise/coderd/coderdenttest/proxytest.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 730f8663a7b26..9db5a030ebc18 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -591,7 +591,7 @@ type HTTPCookieConfig struct { SameSite string `json:"same_site,omitempty" typescript:",notnull"` } -func (cfg HTTPCookieConfig) Apply(c *http.Cookie) *http.Cookie { +func (cfg *HTTPCookieConfig) Apply(c *http.Cookie) *http.Cookie { c.Secure = cfg.Secure.Value() c.SameSite = cfg.HTTPSameSite() return c diff --git a/enterprise/coderd/coderdenttest/proxytest.go b/enterprise/coderd/coderdenttest/proxytest.go index 089bb7c2be99b..3445514f81e50 100644 --- a/enterprise/coderd/coderdenttest/proxytest.go +++ b/enterprise/coderd/coderdenttest/proxytest.go @@ -156,7 +156,7 @@ func NewWorkspaceProxyReplica(t *testing.T, coderdAPI *coderd.API, owner *coders RealIPConfig: coderdAPI.RealIPConfig, Tracing: coderdAPI.TracerProvider, APIRateLimit: coderdAPI.APIRateLimit, - SecureAuthCookie: coderdAPI.SecureAuthCookie, + CookieConfig: coderdAPI.Cookies, ProxySessionToken: token, DisablePathApps: options.DisablePathApps, // We need a new registry to not conflict with the coderd internal From f1b49449defc27f8d2e2c9326b6d76f968bb0256 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 1 Apr 2025 09:00:27 -0500 Subject: [PATCH 06/11] pass in the correct cookie options --- coderd/coderd.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 758336db5ced9..512ea5f3c2037 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -868,7 +868,7 @@ func New(options *Options) *API { r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) { r.Use( apiKeyMiddlewareRedirect, - httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, codersdk.HTTPCookieConfig{}, nil), + httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.Cookies, nil), ) r.Get("/", api.externalAuthCallback(externalAuthConfig)) }) @@ -1123,14 +1123,14 @@ func New(options *Options) *API { r.Get("/github/device", api.userOAuth2GithubDevice) r.Route("/github", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, codersdk.HTTPCookieConfig{}, nil), + httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.Cookies, nil), ) r.Get("/callback", api.userOAuth2Github) }) }) r.Route("/oidc/callback", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, codersdk.HTTPCookieConfig{}, oidcAuthURLParams), + httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.Cookies, oidcAuthURLParams), ) r.Get("/", api.userOIDC) }) From 23f17ae59f4e719a9054699724fc8c6ee5e54b19 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 1 Apr 2025 16:12:53 -0500 Subject: [PATCH 07/11] make gen --- coderd/apidoc/docs.go | 17 ++++++++++++++--- coderd/apidoc/swagger.json | 17 ++++++++++++++--- docs/reference/api/general.md | 5 ++++- docs/reference/api/schemas.md | 28 +++++++++++++++++++++++++--- docs/reference/cli/server.md | 11 +++++++++++ 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index a4ce06d7cb2c3..6bb177d699501 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11902,6 +11902,9 @@ const docTemplate = `{ "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" }, + "http_cookies": { + "$ref": "#/definitions/codersdk.HTTPCookieConfig" + }, "in_memory_database": { "type": "boolean" }, @@ -11962,9 +11965,6 @@ const docTemplate = `{ "scim_api_key": { "type": "string" }, - "secure_auth_cookie": { - "type": "boolean" - }, "session_lifetime": { "$ref": "#/definitions/codersdk.SessionLifetime" }, @@ -12484,6 +12484,17 @@ const docTemplate = `{ } } }, + "codersdk.HTTPCookieConfig": { + "type": "object", + "properties": { + "same_site": { + "type": "string" + }, + "secure_auth_cookie": { + "type": "boolean" + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 37dbcb4b3ec02..de1d4e41c0673 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10642,6 +10642,9 @@ "description": "HTTPAddress is a string because it may be set to zero to disable.", "type": "string" }, + "http_cookies": { + "$ref": "#/definitions/codersdk.HTTPCookieConfig" + }, "in_memory_database": { "type": "boolean" }, @@ -10702,9 +10705,6 @@ "scim_api_key": { "type": "string" }, - "secure_auth_cookie": { - "type": "boolean" - }, "session_lifetime": { "$ref": "#/definitions/codersdk.SessionLifetime" }, @@ -11214,6 +11214,17 @@ } } }, + "codersdk.HTTPCookieConfig": { + "type": "object", + "properties": { + "same_site": { + "type": "string" + }, + "secure_auth_cookie": { + "type": "boolean" + } + } + }, "codersdk.Healthcheck": { "type": "object", "properties": { diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 20372423f12ad..0db339a5baec9 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -260,6 +260,10 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -433,7 +437,6 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index be809670a6d84..8d38d0c4e346b 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1945,6 +1945,10 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -2118,7 +2122,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, @@ -2422,6 +2425,10 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "threshold_database": 0 }, "http_address": "string", + "http_cookies": { + "same_site": "string", + "secure_auth_cookie": true + }, "in_memory_database": true, "job_hang_detector_interval": 0, "logging": { @@ -2595,7 +2602,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o }, "redirect_to_access_url": true, "scim_api_key": "string", - "secure_auth_cookie": true, "session_lifetime": { "default_duration": 0, "default_token_lifetime": 0, @@ -2711,6 +2717,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `external_token_encryption_keys` | array of string | false | | | | `healthcheck` | [codersdk.HealthcheckConfig](#codersdkhealthcheckconfig) | false | | | | `http_address` | string | false | | Http address is a string because it may be set to zero to disable. | +| `http_cookies` | [codersdk.HTTPCookieConfig](#codersdkhttpcookieconfig) | false | | | | `in_memory_database` | boolean | false | | | | `job_hang_detector_interval` | integer | false | | | | `logging` | [codersdk.LoggingConfig](#codersdkloggingconfig) | false | | | @@ -2729,7 +2736,6 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o | `rate_limit` | [codersdk.RateLimitConfig](#codersdkratelimitconfig) | false | | | | `redirect_to_access_url` | boolean | false | | | | `scim_api_key` | string | false | | | -| `secure_auth_cookie` | boolean | false | | | | `session_lifetime` | [codersdk.SessionLifetime](#codersdksessionlifetime) | false | | | | `ssh_keygen_algorithm` | string | false | | | | `strict_transport_security` | integer | false | | | @@ -3298,6 +3304,22 @@ Git clone makes use of this by parsing the URL from: 'Username for "https://gith | ยป `[any property]` | array of string | false | | | | `regex_filter` | [regexp.Regexp](#regexpregexp) | false | | Regex filter is a regular expression that filters the groups returned by the OIDC provider. Any group not matched by this regex will be ignored. If the group filter is nil, then no group filtering will occur. | +## codersdk.HTTPCookieConfig + +```json +{ + "same_site": "string", + "secure_auth_cookie": true +} +``` + +### Properties + +| Name | Type | Required | Restrictions | Description | +|----------------------|---------|----------|--------------|-------------| +| `same_site` | string | false | | | +| `secure_auth_cookie` | boolean | false | | | + ## codersdk.Healthcheck ```json diff --git a/docs/reference/cli/server.md b/docs/reference/cli/server.md index f55165bb397da..1b4052e335e66 100644 --- a/docs/reference/cli/server.md +++ b/docs/reference/cli/server.md @@ -992,6 +992,17 @@ Type of auth to use when connecting to postgres. For AWS RDS, using IAM authenti Controls if the 'Secure' property is set on browser session cookies. +### --samesite-auth-cookie + +| | | +|-------------|--------------------------------------------| +| Type | lax\|none | +| Environment | $CODER_SAMESITE_AUTH_COOKIE | +| YAML | networking.sameSiteAuthCookie | +| Default | lax | + +Controls the 'SameSite' property is set on browser session cookies. + ### --terms-of-service-url | | | From a90eff73ef6ff53a4ab15bcce25f87d074d97467 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 1 Apr 2025 16:29:37 -0500 Subject: [PATCH 08/11] Add unit test to check for secure and samesite cookie flags --- cli/server.go | 1 - coderd/apikey.go | 2 +- coderd/coderd.go | 9 +++--- coderd/coderdtest/oidctest/idp.go | 2 +- coderd/coderdtest/testjar/cookiejar.go | 33 ++++++++++++++++++++++ coderd/userauth.go | 7 ++--- coderd/userauth_test.go | 38 +++++++++++++++++++++++--- 7 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 coderd/coderdtest/testjar/cookiejar.go diff --git a/cli/server.go b/cli/server.go index af3792a069c27..5ea0f4ebbd687 100644 --- a/cli/server.go +++ b/cli/server.go @@ -641,7 +641,6 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. GoogleTokenValidator: googleTokenValidator, ExternalAuthConfigs: externalAuthConfigs, RealIPConfig: realIPConfig, - Cookies: vals.HTTPCookies, SSHKeygenAlgorithm: sshKeygenAlgorithm, TracerProvider: tracerProvider, Telemetry: telemetry.NewNoop(), diff --git a/coderd/apikey.go b/coderd/apikey.go index 2ad0c30fb45c5..ddcf7767719e5 100644 --- a/coderd/apikey.go +++ b/coderd/apikey.go @@ -382,7 +382,7 @@ func (api *API) createAPIKey(ctx context.Context, params apikey.CreateParams) (* APIKeys: []telemetry.APIKey{telemetry.ConvertAPIKey(newkey)}, }) - return api.Cookies.Apply(&http.Cookie{ + return api.DeploymentValues.HTTPCookies.Apply(&http.Cookie{ Name: codersdk.SessionTokenCookie, Value: sessionToken, Path: "/", diff --git a/coderd/coderd.go b/coderd/coderd.go index 512ea5f3c2037..c03c77b518c05 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -155,7 +155,6 @@ type Options struct { GithubOAuth2Config *GithubOAuth2Config OIDCConfig *OIDCConfig PrometheusRegistry *prometheus.Registry - Cookies codersdk.HTTPCookieConfig StrictTransportSecurityCfg httpmw.HSTSConfig SSHKeygenAlgorithm gitsshkey.Algorithm Telemetry telemetry.Reporter @@ -828,7 +827,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, - httpmw.CSRF(options.Cookies), + httpmw.CSRF(options.DeploymentValues.HTTPCookies), ) // This incurs a performance hit from the middleware, but is required to make sure @@ -868,7 +867,7 @@ func New(options *Options) *API { r.Route(fmt.Sprintf("/%s/callback", externalAuthConfig.ID), func(r chi.Router) { r.Use( apiKeyMiddlewareRedirect, - httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.Cookies, nil), + httpmw.ExtractOAuth2(externalAuthConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), ) r.Get("/", api.externalAuthCallback(externalAuthConfig)) }) @@ -1123,14 +1122,14 @@ func New(options *Options) *API { r.Get("/github/device", api.userOAuth2GithubDevice) r.Route("/github", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.Cookies, nil), + httpmw.ExtractOAuth2(options.GithubOAuth2Config, options.HTTPClient, options.DeploymentValues.HTTPCookies, nil), ) r.Get("/callback", api.userOAuth2Github) }) }) r.Route("/oidc/callback", func(r chi.Router) { r.Use( - httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.Cookies, oidcAuthURLParams), + httpmw.ExtractOAuth2(options.OIDCConfig, options.HTTPClient, options.DeploymentValues.HTTPCookies, oidcAuthURLParams), ) r.Get("/", api.userOIDC) }) diff --git a/coderd/coderdtest/oidctest/idp.go b/coderd/coderdtest/oidctest/idp.go index d4f24140b6726..b82f8a00dedb4 100644 --- a/coderd/coderdtest/oidctest/idp.go +++ b/coderd/coderdtest/oidctest/idp.go @@ -1320,7 +1320,7 @@ func (f *FakeIDP) httpHandler(t testing.TB) http.Handler { // requests will fail. func (f *FakeIDP) HTTPClient(rest *http.Client) *http.Client { if f.serve { - if rest == nil || rest.Transport == nil { + if rest == nil { return &http.Client{} } return rest diff --git a/coderd/coderdtest/testjar/cookiejar.go b/coderd/coderdtest/testjar/cookiejar.go new file mode 100644 index 0000000000000..caec922c40ae4 --- /dev/null +++ b/coderd/coderdtest/testjar/cookiejar.go @@ -0,0 +1,33 @@ +package testjar + +import ( + "net/http" + "net/url" + "sync" +) + +func New() *Jar { + return &Jar{} +} + +// Jar exists because 'cookiejar.New()' strips many of the http.Cookie fields +// that are needed to assert. Such as 'Secure' and 'SameSite'. +type Jar struct { + m sync.Mutex + perURL map[string][]*http.Cookie +} + +func (j *Jar) SetCookies(u *url.URL, cookies []*http.Cookie) { + j.m.Lock() + defer j.m.Unlock() + if j.perURL == nil { + j.perURL = make(map[string][]*http.Cookie) + } + j.perURL[u.Host] = append(j.perURL[u.Host], cookies...) +} + +func (j *Jar) Cookies(u *url.URL) []*http.Cookie { + j.m.Lock() + defer j.m.Unlock() + return j.perURL[u.Host] +} diff --git a/coderd/userauth.go b/coderd/userauth.go index 18f1f74a3c66e..91472996737aa 100644 --- a/coderd/userauth.go +++ b/coderd/userauth.go @@ -204,7 +204,7 @@ func (api *API) postConvertLoginType(rw http.ResponseWriter, r *http.Request) { Path: "/", Value: token, Expires: claims.Expiry.Time(), - Secure: api.Cookies.Secure.Value(), + Secure: api.DeploymentValues.HTTPCookies.Secure.Value(), HttpOnly: true, // Must be SameSite to work on the redirected auth flow from the // oauth provider. @@ -1913,13 +1913,12 @@ func (api *API) oauthLogin(r *http.Request, params *oauthLoginParams) ([]*http.C slog.F("user_id", user.ID), ) } - cookies = append(cookies, &http.Cookie{ + cookies = append(cookies, api.DeploymentValues.HTTPCookies.Apply(&http.Cookie{ Name: codersdk.SessionTokenCookie, Path: "/", MaxAge: -1, - Secure: api.Cookies.Secure.Value(), HttpOnly: true, - }) + })) // This is intentional setting the key to the deleted old key, // as the user needs to be forced to log back in. key = *oldKey diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index ddf3dceba236f..712b480dbd807 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/rand" + "crypto/tls" "encoding/json" "fmt" "io" @@ -33,6 +34,7 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest/oidctest" + "github.com/coder/coder/v2/coderd/coderdtest/testjar" "github.com/coder/coder/v2/coderd/cryptokeys" "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/database/dbauthz" @@ -66,8 +68,16 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { cfg.SecondaryClaims = coderd.MergedClaimsSourceNone }) + certificates := []tls.Certificate{testutil.GenerateTLSCertificate(t, "localhost")} client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{ - OIDCConfig: cfg, + OIDCConfig: cfg, + TLSCertificates: certificates, + DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { + values.HTTPCookies = codersdk.HTTPCookieConfig{ + Secure: true, + SameSite: "none", + } + }), }) const username = "alice" @@ -78,15 +88,35 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { "sub": uuid.NewString(), } - helper := oidctest.NewLoginHelper(client, fake) // Signup alice - userClient, _ := helper.Login(t, claims) + freshClient := func() *codersdk.Client { + cli := codersdk.New(client.URL) + cli.HTTPClient.Transport = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + cli.HTTPClient.Jar = testjar.New() + return cli + } + + unauthenticated := freshClient() + userClient, _ := fake.Login(t, unauthenticated, claims) + + cookies := unauthenticated.HTTPClient.Jar.Cookies(client.URL) + require.True(t, len(cookies) > 0) + for _, c := range cookies { + require.Truef(t, c.Secure, "cookie %q", c.Name) + require.Equalf(t, http.SameSiteNoneMode, c.SameSite, "cookie %q", c.Name) + } // Expire the link. This will force the client to refresh the token. + helper := oidctest.NewLoginHelper(userClient, fake) helper.ExpireOauthToken(t, api.Database, userClient) // Instead of refreshing, just log in again. - helper.Login(t, claims) + unauthenticated = freshClient() + fake.Login(t, unauthenticated, claims) } func TestUserLogin(t *testing.T) { From 654fc1e1b0a66c02344b5e69c8624dd799cb3cdf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 1 Apr 2025 16:40:54 -0500 Subject: [PATCH 09/11] compile fix --- enterprise/coderd/coderdenttest/proxytest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise/coderd/coderdenttest/proxytest.go b/enterprise/coderd/coderdenttest/proxytest.go index 3445514f81e50..5aaaf4a88a725 100644 --- a/enterprise/coderd/coderdenttest/proxytest.go +++ b/enterprise/coderd/coderdenttest/proxytest.go @@ -156,7 +156,7 @@ func NewWorkspaceProxyReplica(t *testing.T, coderdAPI *coderd.API, owner *coders RealIPConfig: coderdAPI.RealIPConfig, Tracing: coderdAPI.TracerProvider, APIRateLimit: coderdAPI.APIRateLimit, - CookieConfig: coderdAPI.Cookies, + CookieConfig: coderdAPI.DeploymentValues.HTTPCookies, ProxySessionToken: token, DisablePathApps: options.DisablePathApps, // We need a new registry to not conflict with the coderd internal From a9c22a0440623778eb6cfa676ad3c28555382a20 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 1 Apr 2025 16:51:02 -0500 Subject: [PATCH 10/11] update golden files --- cli/testdata/coder_server_--help.golden | 3 +++ cli/testdata/server-config.yaml.golden | 3 +++ enterprise/cli/testdata/coder_server_--help.golden | 3 +++ 3 files changed, 9 insertions(+) diff --git a/cli/testdata/coder_server_--help.golden b/cli/testdata/coder_server_--help.golden index 7fe70860e2e2a..1cefe8767f3b0 100644 --- a/cli/testdata/coder_server_--help.golden +++ b/cli/testdata/coder_server_--help.golden @@ -251,6 +251,9 @@ NETWORKING OPTIONS: Specifies whether to redirect requests that do not match the access URL host. + --samesite-auth-cookie lax|none, $CODER_SAMESITE_AUTH_COOKIE (default: lax) + Controls the 'SameSite' property is set on browser session cookies. + --secure-auth-cookie bool, $CODER_SECURE_AUTH_COOKIE Controls if the 'Secure' property is set on browser session cookies. diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 271593f753395..911270a579457 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -174,6 +174,9 @@ networking: # Controls if the 'Secure' property is set on browser session cookies. # (default: , type: bool) secureAuthCookie: false + # Controls the 'SameSite' property is set on browser session cookies. + # (default: lax, type: enum[lax\|none]) + sameSiteAuthCookie: lax # Whether Coder only allows connections to workspaces via the browser. # (default: , type: bool) browserOnly: false diff --git a/enterprise/cli/testdata/coder_server_--help.golden b/enterprise/cli/testdata/coder_server_--help.golden index 8f383e145aa94..d11304742d974 100644 --- a/enterprise/cli/testdata/coder_server_--help.golden +++ b/enterprise/cli/testdata/coder_server_--help.golden @@ -252,6 +252,9 @@ NETWORKING OPTIONS: Specifies whether to redirect requests that do not match the access URL host. + --samesite-auth-cookie lax|none, $CODER_SAMESITE_AUTH_COOKIE (default: lax) + Controls the 'SameSite' property is set on browser session cookies. + --secure-auth-cookie bool, $CODER_SECURE_AUTH_COOKIE Controls if the 'Secure' property is set on browser session cookies. From 90758f5085d4c1e7329fd788c74f87d241675050 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 16:28:06 -0500 Subject: [PATCH 11/11] linting --- coderd/userauth_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index 712b480dbd807..7f6dcf771ab5d 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -93,6 +93,7 @@ func TestOIDCOauthLoginWithExisting(t *testing.T) { cli := codersdk.New(client.URL) cli.HTTPClient.Transport = &http.Transport{ TLSClientConfig: &tls.Config{ + //nolint:gosec InsecureSkipVerify: true, }, }