diff --git a/coderd/coderd.go b/coderd/coderd.go index 0e1eb2fe57932..4b5f143bdee9b 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -187,6 +187,7 @@ func New(options *Options) *API { next.ServeHTTP(w, r) }) }, + httpmw.CSRF(options.SecureAuthCookie), ) apps := func(r chi.Router) { diff --git a/coderd/httpapi/cookie_test.go b/coderd/httpapi/cookie_test.go index 546f1d2a7b30c..48c66abc439f0 100644 --- a/coderd/httpapi/cookie_test.go +++ b/coderd/httpapi/cookie_test.go @@ -17,13 +17,13 @@ func TestStripCoderCookies(t *testing.T) { "testing=hello; wow=test", "testing=hello; wow=test", }, { - "session_token=moo; wow=test", + "coder_session_token=moo; wow=test", "wow=test", }, { - "another_token=wow; session_token=ok", + "another_token=wow; coder_session_token=ok", "another_token=wow", }, { - "session_token=ok; oauth_state=wow; oauth_redirect=/", + "coder_session_token=ok; oauth_state=wow; oauth_redirect=/", "", }} { tc := tc diff --git a/coderd/httpmw/apikey.go b/coderd/httpmw/apikey.go index c2b748434f8d5..65c156b53173a 100644 --- a/coderd/httpmw/apikey.go +++ b/coderd/httpmw/apikey.go @@ -123,13 +123,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool httpapi.Write(rw, code, response) } - var cookieValue string - cookie, err := r.Cookie(codersdk.SessionTokenKey) - if err != nil { - cookieValue = r.URL.Query().Get(codersdk.SessionTokenKey) - } else { - cookieValue = cookie.Value - } + cookieValue := apiTokenFromRequest(r) if cookieValue == "" { write(http.StatusUnauthorized, codersdk.Response{ Message: signedOutErrorMessage, @@ -335,3 +329,36 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool }) } } + +// apiTokenFromRequest returns the api token from the request. +// Find the session token from: +// 1: The cookie +// 2: The old cookie +// 3. The coder_session_token query parameter +// 4. The custom auth header +func apiTokenFromRequest(r *http.Request) string { + cookie, err := r.Cookie(codersdk.SessionTokenKey) + if err == nil && cookie.Value != "" { + return cookie.Value + } + + // TODO: @emyrk in October 2022, remove this oldCookie check. + // This is just to support the old cli for 1 release. Then everyone + // must update. + oldCookie, err := r.Cookie("session_token") + if err == nil && oldCookie.Value != "" { + return oldCookie.Value + } + + urlValue := r.URL.Query().Get(codersdk.SessionTokenKey) + if urlValue != "" { + return urlValue + } + + headerValue := r.Header.Get(codersdk.SessionCustomHeader) + if headerValue != "" { + return headerValue + } + + return "" +} diff --git a/coderd/httpmw/apikey_test.go b/coderd/httpmw/apikey_test.go index adc13b2f176fa..fc7bf92af781b 100644 --- a/coderd/httpmw/apikey_test.go +++ b/coderd/httpmw/apikey_test.go @@ -74,10 +74,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: "test-wow-hello", - }) + r.Header.Set(codersdk.SessionCustomHeader, "test-wow-hello") httpmw.ExtractAPIKey(db, nil, false)(successHandler).ServeHTTP(rw, r) res := rw.Result() @@ -92,10 +89,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: "test-wow", - }) + r.Header.Set(codersdk.SessionCustomHeader, "test-wow") httpmw.ExtractAPIKey(db, nil, false)(successHandler).ServeHTTP(rw, r) res := rw.Result() @@ -110,10 +104,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: "testtestid-wow", - }) + r.Header.Set(codersdk.SessionCustomHeader, "testtestid-wow") httpmw.ExtractAPIKey(db, nil, false)(successHandler).ServeHTTP(rw, r) res := rw.Result() @@ -129,10 +120,7 @@ func TestAPIKey(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) httpmw.ExtractAPIKey(db, nil, false)(successHandler).ServeHTTP(rw, r) res := rw.Result() @@ -149,10 +137,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) // Use a different secret so they don't match! hashed := sha256.Sum256([]byte("differentsecret")) @@ -178,10 +163,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -206,10 +188,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -280,10 +259,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -316,10 +292,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -352,10 +325,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -395,10 +365,7 @@ func TestAPIKey(t *testing.T) { rw = httptest.NewRecorder() user = createUser(r.Context(), t, db) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) sentAPIKey, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, @@ -449,10 +416,7 @@ func TestAPIKey(t *testing.T) { user = createUser(r.Context(), t, db) ) r.RemoteAddr = "1.1.1.1:3555" - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) _, err := db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ ID: id, diff --git a/coderd/httpmw/authorize_test.go b/coderd/httpmw/authorize_test.go index dcae5bff96526..32e2742ccd47f 100644 --- a/coderd/httpmw/authorize_test.go +++ b/coderd/httpmw/authorize_test.go @@ -93,10 +93,7 @@ func TestExtractUserRoles(t *testing.T) { }) req := httptest.NewRequest("GET", "/", nil) - req.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: token, - }) + req.Header.Set(codersdk.SessionCustomHeader, token) rtr.ServeHTTP(rw, req) resp := rw.Result() diff --git a/coderd/httpmw/csrf.go b/coderd/httpmw/csrf.go new file mode 100644 index 0000000000000..b4f0880569187 --- /dev/null +++ b/coderd/httpmw/csrf.go @@ -0,0 +1,72 @@ +package httpmw + +import ( + "net/http" + "regexp" + + "github.com/justinas/nosurf" + "golang.org/x/xerrors" + + "github.com/coder/coder/codersdk" +) + +// CSRF is a middleware that verifies that a CSRF token is present in the request +// for non-GET requests. +func CSRF(secureCookie bool) 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.SetFailureHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Something is wrong with your CSRF token. Please refresh the page. If this error persists, try clearing your cookies.", http.StatusBadRequest) + })) + + // Exempt all requests that do not require CSRF protection. + // All GET requests are exempt by default. + mw.ExemptPath("/api/v2/csp/reports") + + // Top level agent routes. + mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/[^/]*$")) + // Agent authenticated routes + mw.ExemptRegexp(regexp.MustCompile("api/v2/workspaceagents/me/*")) + // Derp routes + mw.ExemptRegexp(regexp.MustCompile("derp/*")) + + mw.ExemptFunc(func(r *http.Request) bool { + // Enable CSRF in November 2022 by deleting this "return true" line. + // CSRF is not enforced to ensure backwards compatibility with older + // cli versions. + //nolint:revive + return true + + // CSRF only affects requests that automatically attach credentials via a cookie. + // If no cookie is present, then there is no risk of CSRF. + //nolint:govet + sessCookie, err := r.Cookie(codersdk.SessionTokenKey) + if xerrors.Is(err, http.ErrNoCookie) { + return true + } + + if token := r.Header.Get(codersdk.SessionCustomHeader); token == sessCookie.Value { + // If the cookie and header match, we can assume this is the same as just using the + // custom header auth. Custom header auth can bypass CSRF, as CSRF attacks + // cannot add custom headers. + return true + } + + if token := r.URL.Query().Get(codersdk.SessionTokenKey); token == sessCookie.Value { + // If the auth is set in a url param and matches the cookie, it + // is the same as just using the url param. + return true + } + + // If the X-CSRF-TOKEN header is set, we can exempt the func if it's valid. + // This is the CSRF check. + sent := r.Header.Get("X-CSRF-TOKEN") + if sent != "" { + return nosurf.VerifyToken(nosurf.Token(r), sent) + } + return false + }) + return mw + } +} diff --git a/coderd/httpmw/organizationparam_test.go b/coderd/httpmw/organizationparam_test.go index bdf442391091c..0da000802879f 100644 --- a/coderd/httpmw/organizationparam_test.go +++ b/coderd/httpmw/organizationparam_test.go @@ -29,10 +29,7 @@ func TestOrganizationParam(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) hashed = sha256.Sum256([]byte(secret)) ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/templateparam_test.go b/coderd/httpmw/templateparam_test.go index 7da35889aa536..1e936b403ee5a 100644 --- a/coderd/httpmw/templateparam_test.go +++ b/coderd/httpmw/templateparam_test.go @@ -29,10 +29,7 @@ func TestTemplateParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/templateversionparam_test.go b/coderd/httpmw/templateversionparam_test.go index fe4ebba9dfcc3..6c28cb743f6ff 100644 --- a/coderd/httpmw/templateversionparam_test.go +++ b/coderd/httpmw/templateversionparam_test.go @@ -29,10 +29,7 @@ func TestTemplateVersionParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/userparam_test.go b/coderd/httpmw/userparam_test.go index d21f2a583d58f..0b5e6013c38d2 100644 --- a/coderd/httpmw/userparam_test.go +++ b/coderd/httpmw/userparam_test.go @@ -29,10 +29,7 @@ func TestUserParam(t *testing.T) { r = httptest.NewRequest("GET", "/", nil) rw = httptest.NewRecorder() ) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) user, err := db.InsertUser(r.Context(), database.InsertUserParams{ ID: uuid.New(), diff --git a/coderd/httpmw/workspaceagent.go b/coderd/httpmw/workspaceagent.go index 70331d80a540f..26a7eae41875c 100644 --- a/coderd/httpmw/workspaceagent.go +++ b/coderd/httpmw/workspaceagent.go @@ -29,14 +29,14 @@ func WorkspaceAgent(r *http.Request) database.WorkspaceAgent { func ExtractWorkspaceAgent(db database.Store) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - cookie, err := r.Cookie(codersdk.SessionTokenKey) - if err != nil { + cookieValue := apiTokenFromRequest(r) + if cookieValue == "" { httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{ Message: fmt.Sprintf("Cookie %q must be provided.", codersdk.SessionTokenKey), }) return } - token, err := uuid.Parse(cookie.Value) + token, err := uuid.Parse(cookieValue) if err != nil { httpapi.Write(rw, http.StatusUnauthorized, codersdk.Response{ Message: "Agent token is invalid.", diff --git a/coderd/httpmw/workspaceagent_test.go b/coderd/httpmw/workspaceagent_test.go index eef7fc80847f1..a33bc51bdce9e 100644 --- a/coderd/httpmw/workspaceagent_test.go +++ b/coderd/httpmw/workspaceagent_test.go @@ -22,10 +22,7 @@ func TestWorkspaceAgent(t *testing.T) { setup := func(db database.Store) (*http.Request, uuid.UUID) { token := uuid.New() r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: token.String(), - }) + r.Header.Set(codersdk.SessionCustomHeader, token.String()) return r, token } diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go index c2d047fdda983..435308bd3d4c9 100644 --- a/coderd/httpmw/workspaceagentparam_test.go +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -29,10 +29,7 @@ func TestWorkspaceAgentParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/workspacebuildparam_test.go b/coderd/httpmw/workspacebuildparam_test.go index e16f18417d3a1..c0028347ed3d7 100644 --- a/coderd/httpmw/workspacebuildparam_test.go +++ b/coderd/httpmw/workspacebuildparam_test.go @@ -29,10 +29,7 @@ func TestWorkspaceBuildParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/httpmw/workspaceparam_test.go b/coderd/httpmw/workspaceparam_test.go index 5b3f9a5830bc7..7dfaa55724996 100644 --- a/coderd/httpmw/workspaceparam_test.go +++ b/coderd/httpmw/workspaceparam_test.go @@ -32,10 +32,7 @@ func TestWorkspaceParam(t *testing.T) { hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) @@ -340,10 +337,7 @@ func setupWorkspaceWithAgents(t testing.TB, cfg setupConfig) (database.Store, *h hashed = sha256.Sum256([]byte(secret)) ) r := httptest.NewRequest("GET", "/", nil) - r.AddCookie(&http.Cookie{ - Name: codersdk.SessionTokenKey, - Value: fmt.Sprintf("%s-%s", id, secret), - }) + r.Header.Set(codersdk.SessionCustomHeader, fmt.Sprintf("%s-%s", id, secret)) userID := uuid.New() username, err := cryptorand.String(8) diff --git a/coderd/userauth_test.go b/coderd/userauth_test.go index b4e98a7a86bd4..5c411155904bb 100644 --- a/coderd/userauth_test.go +++ b/coderd/userauth_test.go @@ -245,7 +245,7 @@ func TestUserOAuth2Github(t *testing.T) { resp := oauth2Callback(t, client) require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode) - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(context.Background(), "me") require.NoError(t, err) require.Equal(t, "kyle@coder.com", user.Email) @@ -398,14 +398,14 @@ func TestUserOIDC(t *testing.T) { defer cancel() if tc.Username != "" { - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.Username, user.Username) } if tc.AvatarURL != "" { - client.SessionToken = resp.Cookies()[0].Value + client.SessionToken = authCookieValue(resp.Cookies()) user, err := client.User(ctx, "me") require.NoError(t, err) require.Equal(t, tc.AvatarURL, user.AvatarURL) @@ -534,3 +534,12 @@ func oidcCallback(t *testing.T, client *codersdk.Client) *http.Response { func i64ptr(i int64) *int64 { return &i } + +func authCookieValue(cookies []*http.Cookie) string { + for _, cookie := range cookies { + if cookie.Name == codersdk.SessionTokenKey { + return cookie.Value + } + } + return "" +} diff --git a/coderd/users_test.go b/coderd/users_test.go index cf2cfd6a7866a..6f1c2d7ebba37 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -330,11 +330,16 @@ func TestPostLogout(t *testing.T) { require.Equal(t, http.StatusOK, res.StatusCode) cookies := res.Cookies() - require.Len(t, cookies, 2, "Exactly two cookies should be returned") - require.Equal(t, codersdk.SessionTokenKey, cookies[0].Name, "Cookie should be the auth & app cookie") - require.Equal(t, codersdk.SessionTokenKey, cookies[1].Name, "Cookie should be the auth & app cookie") - require.Equal(t, -1, cookies[0].MaxAge, "Cookie should be set to delete") + var found bool + for _, cookie := range cookies { + if cookie.Name == codersdk.SessionTokenKey { + require.Equal(t, codersdk.SessionTokenKey, cookie.Name, "Cookie should be the auth cookie") + require.Equal(t, -1, cookie.MaxAge, "Cookie should be set to delete") + found = true + } + } + require.True(t, found, "auth cookie should be returned") _, err = client.GetAPIKey(ctx, admin.UserID.String(), keyID) sdkErr := &codersdk.Error{} diff --git a/codersdk/client.go b/codersdk/client.go index 7bc28616c3ecc..6fa34e91b3e06 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -20,9 +20,11 @@ import ( // Be sure to strip additional cookies in httpapi.StripCoder Cookies! const ( // SessionTokenKey represents the name of the cookie or query parameter the API key is stored in. - SessionTokenKey = "session_token" - OAuth2StateKey = "oauth_state" - OAuth2RedirectKey = "oauth_redirect" + SessionTokenKey = "coder_session_token" + // SessionCustomHeader is the custom header to use for authentication. + SessionCustomHeader = "Coder-Session-Token" + OAuth2StateKey = "oauth_state" + OAuth2RedirectKey = "oauth_redirect" ) // New creates a Coder client for the provided URL. @@ -70,10 +72,15 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac if err != nil { return nil, xerrors.Errorf("create request: %w", err) } + req.Header.Set(SessionCustomHeader, c.SessionToken) + + // Delete this custom cookie set in November 2022. This is just to remain + // backwards compatible with older versions of Coder. req.AddCookie(&http.Cookie{ - Name: SessionTokenKey, + Name: "session_token", Value: c.SessionToken, }) + if body != nil { req.Header.Set("Content-Type", "application/json") } diff --git a/go.mod b/go.mod index 13832bcfd4cf2..23576413066d0 100644 --- a/go.mod +++ b/go.mod @@ -59,7 +59,7 @@ replace tailscale.com => github.com/coder/tailscale v1.1.1-0.20220912224234-e80c replace github.com/gliderlabs/ssh => github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 // Fixes a deadlock on close in devtunnel. -replace golang.zx2c4.com/wireguard => github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175 +replace golang.zx2c4.com/wireguard => github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9 require ( cdr.dev/slog v1.4.2-0.20220525200111-18dce5c2cd5f diff --git a/go.sum b/go.sum index e3d95afa7bd3a..41957b92a542a 100644 --- a/go.sum +++ b/go.sum @@ -357,8 +357,8 @@ github.com/coder/ssh v0.0.0-20220811105153-fcea99919338 h1:tN5GKFT68YLVzJoA8AHui github.com/coder/ssh v0.0.0-20220811105153-fcea99919338/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= github.com/coder/tailscale v1.1.1-0.20220912224234-e80caec6c05f h1:NN9O1Pgno2QQy+JBnZk1VQ3vyAmWaB+yEotUDEuFKm8= github.com/coder/tailscale v1.1.1-0.20220912224234-e80caec6c05f/go.mod h1:5amxy08qijEa8bcTW2SeIy4MIqcmd7LMsuOxqOlj2Ak= -github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175 h1:obgyZIctZKcztM+TiIBUIJkOf04L9Fg+oLb47XEGy44= -github.com/coder/wireguard-go v0.0.0-20220913030355-902de6e9b175/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= +github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9 h1:AeU4w8hSB+XEj3e8HjvEUTy/MWQd6tddnr9dELrRjKk= +github.com/coder/wireguard-go v0.0.0-20220913030931-b1b3bb45caf9/go.mod h1:enML0deDxY1ux+B6ANGiwtg0yAJi1rctkTpcHNAVPyg= github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 46a0d000d390c..3e2ea86c16d8b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -4,6 +4,41 @@ import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" +export const hardCodedCSRFCookie = (): string => { + // This is a hard coded CSRF token/cookie pair for local development. + // In prod, the GoLang webserver generates a random cookie with a new token for + // each document request. For local development, we don't use the Go webserver for static files, + // so this is the 'hack' to make local development work with remote apis. + // The CSRF cookie for this token is "JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=" + const csrfToken = + "KNKvagCBEHZK7ihe2t7fj6VeJ0UyTDco1yVUJE8N06oNqxLu5Zx1vRxZbgfC0mJJgeGkVjgs08mgPbcWPBkZ1A==" + axios.defaults.headers.common["X-CSRF-TOKEN"] = csrfToken + return csrfToken +} + +// Always attach CSRF token to all requests. +// In puppeteer the document is undefined. In those cases, just +// do nothing. +const token = + typeof document !== "undefined" + ? document.head.querySelector('meta[property="csrf-token"]') + : null + +if (token !== null && token.getAttribute("content") !== null) { + if (process.env.NODE_ENV === "development") { + // Development mode uses a hard-coded CSRF token + axios.defaults.headers.common["X-CSRF-TOKEN"] = hardCodedCSRFCookie() + token.setAttribute("content", hardCodedCSRFCookie()) + } else { + axios.defaults.headers.common["X-CSRF-TOKEN"] = token.getAttribute("content") ?? "" + } +} else { + // Do not write error logs if we are in a FE unit test. + if (process.env.JEST_WORKER_ID === undefined) { + console.error("CSRF token not found") + } +} + const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", } diff --git a/site/webpack.dev.ts b/site/webpack.dev.ts index cb43015edd7df..9e59efa6aa3a9 100644 --- a/site/webpack.dev.ts +++ b/site/webpack.dev.ts @@ -51,6 +51,12 @@ const config: Configuration = { }, devMiddleware: { publicPath: "/", + headers: { + // This header corresponds to "src/api/api.ts"'s hardcoded FE token. + // This is the secret side of the CSRF double cookie submit method. + "Set-Cookie": + "csrf_token=JXm9hOUdZctWt0ZZGAy9xiS/gxMKYOThdxjjMnMUyn4=; Path=/; HttpOnly; SameSite=Lax", + }, }, headers: { "Access-Control-Allow-Origin": "*",