package browser import ( "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" ) func TestSetRefreshCookieUsesHardenedHostOnlyPolicy(t *testing.T) { t.Setenv("API_ENDPOINT", "/api/v1") req := httptest.NewRequest(http.MethodPost, "http://app.sendico.io/api/v1/accounts/login", nil) req.Header.Set(TransportHeader, TransportCookie) req.Header.Set("X-Forwarded-Proto", "https") recorder := httptest.NewRecorder() SetRefreshCookie(recorder, req, "secret-refresh-token", time.Now().Add(time.Hour)) cookies := recorder.Result().Cookies() require.Len(t, cookies, 1) cookie := cookies[0] require.Equal(t, RefreshCookie, cookie.Name) require.Equal(t, "secret-refresh-token", cookie.Value) require.Equal(t, "/api/v1/accounts", cookie.Path) require.Empty(t, cookie.Domain, "an omitted Domain attribute keeps the cookie host-only") require.True(t, cookie.HttpOnly) require.True(t, cookie.Secure) require.Equal(t, http.SameSiteStrictMode, cookie.SameSite) require.Positive(t, cookie.MaxAge) } func TestSetRefreshCookieUsesInstalledScopeOutsideAccountsRouter(t *testing.T) { t.Setenv("API_ENDPOINT", "/gateway/v2/") req := httptest.NewRequest(http.MethodPost, "https://app.sendico.io/gateway/v2/verification/login", nil) req.Header.Set(TransportHeader, TransportCookie) recorder := httptest.NewRecorder() Scope("/gateway/v2")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { SetRefreshCookie(w, r, "refresh", time.Now().Add(time.Hour)) })).ServeHTTP(recorder, req) require.Equal(t, "/gateway/v2/accounts", recorder.Result().Cookies()[0].Path) } func TestRefreshCookieIsAlwaysSecure(t *testing.T) { t.Setenv("API_ENDPOINT", "/api/v1") tests := []struct { name string target string forwardedProto string }{ {name: "public hostname", target: "http://app.sendico.io/api/v1/accounts/login"}, {name: "localhost", target: "http://localhost:4000/api/v1/accounts/login"}, {name: "loopback IP", target: "http://127.0.0.1:4000/api/v1/accounts/login"}, {name: "private IP", target: "http://192.168.1.20:4000/api/v1/accounts/login"}, {name: "forwarded HTTP", target: "http://app.sendico.io/api/v1/accounts/login", forwardedProto: "http"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { req := httptest.NewRequest(http.MethodPost, test.target, nil) req.Header.Set(TransportHeader, TransportCookie) if test.forwardedProto != "" { req.Header.Set("X-Forwarded-Proto", test.forwardedProto) } recorder := httptest.NewRecorder() SetRefreshCookie(recorder, req, "refresh", time.Now().Add(time.Hour)) require.True(t, recorder.Result().Cookies()[0].Secure) }) } } func TestClearRefreshCookieUsesHardenedPolicy(t *testing.T) { t.Setenv("API_ENDPOINT", "/api/v1") req := httptest.NewRequest(http.MethodPost, "http://192.168.1.20:4000/api/v1/accounts/logout", nil) req.Header.Set(TransportHeader, TransportCookie) recorder := httptest.NewRecorder() ClearRefreshCookie(recorder, req) cookies := recorder.Result().Cookies() require.Len(t, cookies, 1) cookie := cookies[0] require.Equal(t, RefreshCookie, cookie.Name) require.Empty(t, cookie.Value) require.Equal(t, -1, cookie.MaxAge) require.True(t, cookie.HttpOnly) require.True(t, cookie.Secure) require.Equal(t, http.SameSiteStrictMode, cookie.SameSite) } func TestValidateCSRFRejectsCrossSiteAndMissingHeaderRequests(t *testing.T) { allowed := []string{"https://app.sendico.io"} missingHeader := httptest.NewRequest(http.MethodPost, "https://app.sendico.io/api/v1/payments", nil) missingHeader.Header.Set(TransportHeader, TransportCookie) missingHeader.Header.Set("Origin", allowed[0]) require.Error(t, ValidateCSRF(missingHeader, allowed)) crossSite := httptest.NewRequest(http.MethodPost, "https://app.sendico.io/api/v1/payments", nil) crossSite.Header.Set(TransportHeader, TransportCookie) crossSite.Header.Set(CSRFHeader, CSRFValue) crossSite.Header.Set("Origin", "https://attacker.example") require.Error(t, ValidateCSRF(crossSite, allowed)) valid := httptest.NewRequest(http.MethodPost, "https://app.sendico.io/api/v1/payments", nil) valid.Header.Set(TransportHeader, TransportCookie) valid.Header.Set(CSRFHeader, CSRFValue) valid.Header.Set("Origin", allowed[0]) require.NoError(t, ValidateCSRF(valid, allowed)) } func TestRefreshCredentialNeverFallsBackForCookieTransport(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "https://app.sendico.io/api/v1/accounts/refresh", nil) req.Header.Set(TransportHeader, TransportCookie) credential, err := RefreshCredential(req, "body-token") require.Error(t, err) require.Empty(t, credential) }