Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
190 lines
7.2 KiB
Go
190 lines
7.2 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package nosurfx_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ory/kratos/x/nosurfx"
|
|
|
|
"github.com/tidwall/gjson"
|
|
|
|
"github.com/ory/x/assertx"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/kratos/driver/config"
|
|
"github.com/ory/kratos/pkg"
|
|
"github.com/ory/nosurf"
|
|
"github.com/ory/x/randx"
|
|
)
|
|
|
|
func TestNosurfBaseCookieHandler(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, "http://foo.com/bar"))
|
|
|
|
cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil))
|
|
assert.EqualValues(t, "csrf_token_01c86631efd1537ee34a98e75884a6e21dd8e2d9e944934bca21204106bfd32f", cookie.Name, "base64 representation of http://foo.com/bar")
|
|
assert.EqualValues(t, http.SameSiteLaxMode, cookie.SameSite, "is set to lax because https/secure is false - chrome rejects none samesite on non-https")
|
|
assert.EqualValues(t, nosurf.MaxAge, cookie.MaxAge)
|
|
assert.EqualValues(t, "/", cookie.Path, "cookie path is site root by default")
|
|
assert.EqualValues(t, "", cookie.Domain, "domain for the cookie is set to empty per default")
|
|
assert.False(t, cookie.Secure, "false because insecure dev mode")
|
|
assert.True(t, cookie.HttpOnly)
|
|
|
|
alNum := regexp.MustCompile("[a-zA-Z_0-9]+")
|
|
for i := 0; i < 10; i++ {
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, randx.MustString(16, randx.AlphaNum)))
|
|
cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil))
|
|
|
|
assert.NotEqual(t, "aHR0cDovL2Zvby5jb20vYmFy_csrf_token", cookie.Name, "should no longer be http://foo.com/bar")
|
|
assert.True(t, alNum.MatchString(cookie.Name), "does not have any special chars")
|
|
}
|
|
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyCookieSameSite, "None"))
|
|
require.NoError(t, conf.Set(ctx, "dev", false))
|
|
cookie = nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "https://foo/bar", nil))
|
|
assert.EqualValues(t, http.SameSiteNoneMode, cookie.SameSite, "can be none because https/secure is true")
|
|
assert.True(t, cookie.Secure, "true because secure mode")
|
|
assert.True(t, cookie.HttpOnly)
|
|
}
|
|
|
|
func TestNosurfBaseCookieHandlerAliasing(t *testing.T) {
|
|
ctx := context.Background()
|
|
conf, reg := pkg.NewFastRegistryWithMocks(t)
|
|
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyPublicBaseURL, "http://foo.com/bar"))
|
|
|
|
cookie := nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "http://foo.com/bar", nil))
|
|
assert.EqualValues(t, "", cookie.Domain, "remains unset")
|
|
assert.EqualValues(t, "/", cookie.Path, "cookie path is site root by default")
|
|
|
|
// Check root settings
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyCookieDomain, "bar.com"))
|
|
require.NoError(t, conf.Set(ctx, config.ViperKeyCookiePath, "/baz"))
|
|
cookie = nosurfx.NosurfBaseCookieHandler(reg)(httptest.NewRecorder(), httptest.NewRequest("GET", "http://foo.com/bar", nil))
|
|
assert.EqualValues(t, "bar.com", cookie.Domain, "domain doesn't change when request not from an alias but is overwritten by ViperKeyCookieDomain")
|
|
assert.EqualValues(t, "/baz", cookie.Path, "cookie path is site root by default but is overwritten by ViperKeyCookiePath")
|
|
}
|
|
|
|
func TestNosurfBaseCookieErrorHandler(t *testing.T) {
|
|
_, reg := pkg.NewFastRegistryWithMocks(t)
|
|
|
|
h := nosurfx.CSRFFailureHandler(reg)
|
|
expectError := func(t *testing.T, err error, req *http.Request) {
|
|
t.Helper()
|
|
rec := httptest.NewRecorder()
|
|
h(rec, req)
|
|
assertx.EqualAsJSON(t, err, json.RawMessage(gjson.Get(rec.Body.String(), "error").Raw))
|
|
}
|
|
|
|
newAjaxFormRequest := func() *http.Request {
|
|
req := httptest.NewRequest("GET", "https://foo/bar", nil)
|
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
return req
|
|
}
|
|
|
|
newAjaxJSONRequest := func() *http.Request {
|
|
req := httptest.NewRequest("GET", "https://foo/bar", nil)
|
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return req
|
|
}
|
|
|
|
newBrowserRequest := func() *http.Request {
|
|
req := httptest.NewRequest("GET", "https://foo/bar", nil)
|
|
return req
|
|
}
|
|
|
|
t.Run("case=without cookie", func(t *testing.T) {
|
|
t.Run("source=ajax", func(t *testing.T) {
|
|
expectError(t, nosurfx.ErrInvalidCSRFTokenAJAXNoCookies(), newAjaxFormRequest())
|
|
})
|
|
|
|
t.Run("source=browser", func(t *testing.T) {
|
|
expectError(t, nosurfx.ErrInvalidCSRFTokenServerNoCookies(), newBrowserRequest())
|
|
})
|
|
})
|
|
|
|
t.Run("case=ajax with cookie but without csrf cookie", func(t *testing.T) {
|
|
test := func(t *testing.T, req *http.Request, err error) {
|
|
req.Header.Set("Cookie", "foo=bar;")
|
|
expectError(t, err, req)
|
|
}
|
|
|
|
t.Run("source=ajax", func(t *testing.T) {
|
|
test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXCookieMissing())
|
|
})
|
|
|
|
t.Run("source=browser", func(t *testing.T) {
|
|
test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerCookieMissing())
|
|
})
|
|
})
|
|
|
|
t.Run("case=with correct cookie but token was not sent in header", func(t *testing.T) {
|
|
test := func(t *testing.T, req *http.Request, err error) {
|
|
req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;")
|
|
expectError(t, err, req)
|
|
}
|
|
|
|
t.Run("source=ajax", func(t *testing.T) {
|
|
test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenNotSent())
|
|
})
|
|
|
|
t.Run("source=ajax json", func(t *testing.T) {
|
|
test(t, newAjaxJSONRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch())
|
|
})
|
|
|
|
t.Run("source=browser", func(t *testing.T) {
|
|
test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerTokenNotSent())
|
|
})
|
|
})
|
|
|
|
t.Run("case=ajax with correct cookie and token in header but they do not match", func(t *testing.T) {
|
|
test := func(t *testing.T, req *http.Request, err error) {
|
|
req.Header.Set(nosurf.HeaderName, "bar")
|
|
req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;")
|
|
expectError(t, err, req)
|
|
}
|
|
|
|
t.Run("source=ajax", func(t *testing.T) {
|
|
test(t, newAjaxFormRequest(), nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch())
|
|
})
|
|
|
|
t.Run("source=browser", func(t *testing.T) {
|
|
test(t, newBrowserRequest(), nosurfx.ErrInvalidCSRFTokenServerTokenMismatch())
|
|
})
|
|
})
|
|
|
|
t.Run("case=correct cookie and token in body but they do not match", func(t *testing.T) {
|
|
test := func(t *testing.T, req *http.Request, err error) {
|
|
req.Header.Set("Accept", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.Header.Set("Cookie", nosurfx.CSRFCookieName(req.Context(), reg)+"=bar;")
|
|
require.NoError(t, req.ParseForm())
|
|
expectError(t, err, req)
|
|
}
|
|
|
|
t.Run("source=ajax", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "https://foo/bar", strings.NewReader(url.Values{nosurf.FormFieldName: {"bar"}}.Encode()))
|
|
req.Header.Set("Sec-Fetch-Mode", "cors")
|
|
test(t, req, nosurfx.ErrInvalidCSRFTokenAJAXTokenMismatch())
|
|
})
|
|
|
|
t.Run("source=browser", func(t *testing.T) {
|
|
req := httptest.NewRequest("POST", "https://foo/bar", strings.NewReader(url.Values{nosurf.FormFieldName: {"bar"}}.Encode()))
|
|
test(t, req, nosurfx.ErrInvalidCSRFTokenServerTokenMismatch())
|
|
})
|
|
})
|
|
}
|