mirror of https://github.com/coder/coder.git
feat: Implement (but not enforce) CSRF for FE requests (#3786)
Future work is to enforce CSRF Co-authored-by: Presley Pizzo <presley@coder.com>
This commit is contained in:
parent
9ab437d6e2
commit
9b5ee8f267
|
@ -187,6 +187,7 @@ func New(options *Options) *API {
|
|||
next.ServeHTTP(w, r)
|
||||
})
|
||||
},
|
||||
httpmw.CSRF(options.SecureAuthCookie),
|
||||
)
|
||||
|
||||
apps := func(r chi.Router) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
2
go.mod
2
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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": "*",
|
||||
|
|
Loading…
Reference in New Issue