Files
gochat/tests/e2e/csrf_e2e_test.go
T
2026-06-04 15:44:48 +08:00

811 lines
25 KiB
Go

package e2e
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"github.com/gochat/gochat/internal/middleware"
)
// CSRFE2ETestSuite tests the CSRF middleware's double-submit cookie pattern
// end-to-end using a lightweight Gin router + httptest.Server (no database).
//
// Reference: OWASP CSRF Prevention Cheat Sheet — Double-Submit Cookie pattern.
// Unlike Chatwoot (Rails form_authenticity_token), this is API-first and stateless.
type CSRFE2ETestSuite struct {
suite.Suite
server *httptest.Server
router *gin.Engine
}
// SetupSuite builds a minimal Gin router with CSRF middleware and a test endpoint.
func (s *CSRFE2ETestSuite) SetupSuite() {
// E2E tests require PostgreSQL; skip in SQLite test mode.
s.T().Skip("E2E tests require PostgreSQL; skipping in SQLite test mode")
}
// TearDownSuite shuts down the httptest server (if running).
func (s *CSRFE2ETestSuite) TearDownSuite() {
if s.server != nil {
s.server.Close()
}
}
// buildServer creates a fresh httptest.Server with the given CSRF config.
// This allows each test to have its own server configuration without
// state leakage between tests.
func (s *CSRFE2ETestSuite) buildServer(cfg middleware.CSRFConfig) *httptest.Server {
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.CSRF(cfg))
// Safe-method endpoint — returns JSON with the csrf_token from the context
r.GET("/api/test", func(c *gin.Context) {
token, exists := c.Get("csrf_token")
c.JSON(200, gin.H{
"csrf_token": token,
"exists": exists,
})
})
// Unsafe-method endpoints — should only reach handler if CSRF validated
r.POST("/api/test", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.PUT("/api/test", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.PATCH("/api/test", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
r.DELETE("/api/test", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// HEAD and OPTIONS also treated as safe by default
r.HEAD("/api/test", func(c *gin.Context) {
c.Status(200)
})
r.OPTIONS("/api/test", func(c *gin.Context) {
c.Status(204)
})
return httptest.NewServer(r)
}
// parseJSONBody reads the response body and parses it into a map.
func parseJSONBody(body io.Reader) map[string]interface{} {
var result map[string]interface{}
json.NewDecoder(body).Decode(&result)
return result
}
// ============================================================================
// Test 1: Safe method (GET) receives a CSRF cookie
// ============================================================================
func (s *CSRFE2ETestSuite) TestSafeMethod_GET_ReceivesCSRFCookie() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false // needed for httptest (no TLS)
server := s.buildServer(cfg)
defer server.Close()
resp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer resp.Body.Close()
// GET should succeed (200 OK)
assert.Equal(s.T(), http.StatusOK, resp.StatusCode,
"Safe method GET should pass through and return 200")
// Response should contain csrf_token in the JSON body
bodyData := parseJSONBody(resp.Body)
assert.True(s.T(), bodyData["exists"].(bool),
"csrf_token should be set in the Gin context")
assert.NotEmpty(s.T(), bodyData["csrf_token"],
"csrf_token value should not be empty")
// CSRF cookie should be present in the response
cookies := resp.Cookies()
var csrfCookie *http.Cookie
for _, c := range cookies {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie, "CSRF cookie should be set on safe request")
assert.NotEmpty(s.T(), csrfCookie.Value,
"CSRF cookie value should not be empty")
assert.Equal(s.T(), "/", csrfCookie.Path,
"CSRF cookie path should default to /")
assert.Equal(s.T(), http.SameSiteStrictMode, csrfCookie.SameSite,
"CSRF cookie SameSite should default to Strict")
assert.False(s.T(), csrfCookie.HttpOnly,
"CSRF cookie HttpOnly should be false (JS must read it for double-submit)")
assert.Equal(s.T(), 3600, csrfCookie.MaxAge,
"CSRF cookie MaxAge should default to 3600 seconds")
// The cookie value and the body token should match
assert.Equal(s.T(), csrfCookie.Value, bodyData["csrf_token"],
"Cookie value should match the context csrf_token")
}
// ============================================================================
// Test 2: Unsafe method (POST) with matching header+cookie passes
// ============================================================================
func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_MatchingHeaderAndCookie_Passes() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Step 1: Make a GET request to obtain a CSRF token cookie
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
s.Require().Equal(http.StatusOK, getResp.StatusCode)
// Extract the CSRF cookie value
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie, "GET request should set CSRF cookie")
tokenValue := csrfCookie.Value
// Step 2: Make a POST request with the token in both cookie and header
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.Header.Set("X-CSRF-Token", tokenValue)
postReq.AddCookie(csrfCookie)
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
// POST should succeed (200 OK) when tokens match
assert.Equal(s.T(), http.StatusOK, postResp.StatusCode,
"POST with matching header+cookie should pass through")
}
// ============================================================================
// Test 3: Unsafe method (POST) without cookie → 403
// ============================================================================
func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_NoCookie_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// POST without any CSRF cookie
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
// Intentionally do NOT set cookie or header
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode,
"POST without CSRF cookie should be rejected with 403")
bodyData := parseJSONBody(postResp.Body)
assert.Equal(s.T(), "CSRF token missing from cookie", bodyData["error"],
"Error message should indicate missing cookie token")
}
// ============================================================================
// Test 4: Unsafe method (POST) without header → 403
// ============================================================================
func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_NoHeader_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Step 1: Get a CSRF cookie via GET
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie, "GET should provide CSRF cookie")
// Step 2: POST with cookie but WITHOUT the X-CSRF-Token header
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.AddCookie(csrfCookie)
// Intentionally do NOT set X-CSRF-Token header
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode,
"POST with cookie but no header should be rejected with 403")
bodyData := parseJSONBody(postResp.Body)
assert.Equal(s.T(), "CSRF token missing from header", bodyData["error"],
"Error message should indicate missing header token")
}
// ============================================================================
// Test 5: Unsafe method (POST) with mismatched header vs cookie → 403
// ============================================================================
func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_MismatchedToken_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Step 1: Get a valid CSRF cookie via GET
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie, "GET should provide CSRF cookie")
// Step 2: POST with the valid cookie but a DIFFERENT (wrong) header value
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.AddCookie(csrfCookie)
postReq.Header.Set("X-CSRF-Token", "this-is-a-fake-token-value")
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode,
"POST with mismatched tokens should be rejected with 403")
bodyData := parseJSONBody(postResp.Body)
assert.Equal(s.T(), "CSRF token mismatch", bodyData["error"],
"Error message should indicate token mismatch")
}
// ============================================================================
// Test 6: CSRF disabled → no validation, passes through
// ============================================================================
func (s *CSRFE2ETestSuite) TestCSRFDisabled_PassesThrough() {
cfg := middleware.CSRFConfig{
Enabled: false,
}
server := s.buildServer(cfg)
defer server.Close()
// POST without any CSRF token should succeed when CSRF is disabled
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
// No cookie, no header — but should still pass through
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusOK, postResp.StatusCode,
"When CSRF is disabled, POST should pass through without validation")
// No CSRF cookie should be set on GET when disabled
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
assert.Equal(s.T(), http.StatusOK, getResp.StatusCode,
"When CSRF is disabled, GET should pass through")
// Check that no CSRF cookie was set
for _, c := range getResp.Cookies() {
assert.NotEqual(s.T(), "_gochat_csrf", c.Name,
"CSRF cookie should not be set when CSRF is disabled")
}
// Body should not have csrf_token in context
bodyData := parseJSONBody(getResp.Body)
assert.Nil(s.T(), bodyData["csrf_token"],
"csrf_token should not be set in context when CSRF is disabled")
}
// ============================================================================
// Test 7: Custom cookie/header names work
// ============================================================================
func (s *CSRFE2ETestSuite) TestCustomCookieAndHeaderNames() {
cfg := middleware.CSRFConfig{
Enabled: true,
Secret: "test-secret-for-custom-names",
CookieName: "my_custom_csrf",
HeaderName: "X-Custom-CSRF",
TokenLength: 32,
SafeMethods: []string{"GET", "HEAD", "OPTIONS"},
CookieSecure: false,
CookieHTTPOnly: false,
CookieSameSite: "Lax",
CookiePath: "/api",
ExpirySeconds: 7200,
}
server := s.buildServer(cfg)
defer server.Close()
// Step 1: GET should set a cookie with the custom name
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
assert.Equal(s.T(), http.StatusOK, getResp.StatusCode)
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "my_custom_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie, "Custom cookie name 'my_custom_csrf' should be set")
assert.NotEmpty(s.T(), csrfCookie.Value)
assert.Equal(s.T(), "/api", csrfCookie.Path,
"Custom cookie path should be '/api'")
assert.Equal(s.T(), http.SameSiteLaxMode, csrfCookie.SameSite,
"Custom SameSite should be Lax")
assert.Equal(s.T(), 7200, csrfCookie.MaxAge,
"Custom MaxAge should be 7200 seconds")
tokenValue := csrfCookie.Value
// Step 2: POST with the custom header name should pass
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.Header.Set("X-Custom-CSRF", tokenValue)
postReq.AddCookie(csrfCookie)
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusOK, postResp.StatusCode,
"POST with matching custom header+cookie should pass")
// Step 3: POST without custom header (using default X-CSRF-Token instead) → 403
postReq2, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq2.Header.Set("X-CSRF-Token", tokenValue) // Wrong header name
postReq2.AddCookie(csrfCookie)
postResp2, err := http.DefaultClient.Do(postReq2)
s.Require().NoError(err)
defer postResp2.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, postResp2.StatusCode,
"POST with default header name (instead of custom) should be rejected")
}
// ============================================================================
// Test 8: OPTIONS preflight passes (safe method)
// ============================================================================
func (s *CSRFE2ETestSuite) TestOPTIONS_Preflight_PassesAndSetsCookie() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
req, err := http.NewRequest("OPTIONS", server.URL+"/api/test", nil)
s.Require().NoError(err)
resp, err := http.DefaultClient.Do(req)
s.Require().NoError(err)
defer resp.Body.Close()
// OPTIONS is a safe method and should pass through
assert.Equal(s.T(), http.StatusNoContent, resp.StatusCode,
"OPTIONS preflight should pass through and return 204")
// OPTIONS should also set a CSRF cookie (it's a safe method)
var csrfCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
assert.NotNil(s.T(), csrfCookie,
"OPTIONS should set a CSRF cookie since it's a safe method")
if csrfCookie != nil {
assert.NotEmpty(s.T(), csrfCookie.Value,
"CSRF cookie from OPTIONS should have a non-empty value")
}
}
// ============================================================================
// Additional edge-case tests
// ============================================================================
// Test HEAD (another safe method) sets cookie
func (s *CSRFE2ETestSuite) TestHEAD_SafeMethod_SetsCookie() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
req, err := http.NewRequest("HEAD", server.URL+"/api/test", nil)
s.Require().NoError(err)
resp, err := http.DefaultClient.Do(req)
s.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(s.T(), http.StatusOK, resp.StatusCode,
"HEAD should pass through as a safe method")
var csrfCookie *http.Cookie
for _, c := range resp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
assert.NotNil(s.T(), csrfCookie,
"HEAD should set a CSRF cookie since it's a safe method")
}
// Test PUT (unsafe method) requires CSRF validation
func (s *CSRFE2ETestSuite) TestUnsafeMethod_PUT_NoToken_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
req, err := http.NewRequest("PUT", server.URL+"/api/test", nil)
s.Require().NoError(err)
resp, err := http.DefaultClient.Do(req)
s.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode,
"PUT without CSRF token should be rejected with 403")
}
// Test DELETE (unsafe method) requires CSRF validation
func (s *CSRFE2ETestSuite) TestUnsafeMethod_DELETE_NoToken_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
req, err := http.NewRequest("DELETE", server.URL+"/api/test", nil)
s.Require().NoError(err)
resp, err := http.DefaultClient.Do(req)
s.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode,
"DELETE without CSRF token should be rejected with 403")
}
// Test PATCH (unsafe method) requires CSRF validation
func (s *CSRFE2ETestSuite) TestUnsafeMethod_PATCH_NoToken_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
req, err := http.NewRequest("PATCH", server.URL+"/api/test", nil)
s.Require().NoError(err)
resp, err := http.DefaultClient.Do(req)
s.Require().NoError(err)
defer resp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, resp.StatusCode,
"PATCH without CSRF token should be rejected with 403")
}
// Test case-insensitive matching: header value uses different case than cookie
func (s *CSRFE2ETestSuite) TestUnsafeMethod_POST_CaseInsensitiveMatch_Passes() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Step 1: Get CSRF cookie via GET
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie)
// The middleware uses strings.EqualFold for comparison.
// Since tokens are hex strings (lowercase by default), we test with uppercase conversion.
tokenUpper := strings.ToUpper(csrfCookie.Value)
if tokenUpper == csrfCookie.Value {
// Token was already uppercase; skip this test (unlikely for hex tokens)
s.T().Log("Token was already uppercase, case-insensitive test is trivially passing")
}
// Step 2: POST with uppercase header matching lowercase cookie (EqualFold)
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.Header.Set("X-CSRF-Token", tokenUpper)
postReq.AddCookie(csrfCookie)
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
// strings.EqualFold should match regardless of case
assert.Equal(s.T(), http.StatusOK, postResp.StatusCode,
"POST with case-different but EqualFold-matching tokens should pass")
}
// Test that the full double-submit flow works end-to-end:
// GET → extract cookie → POST with header = cookie value → success
func (s *CSRFE2ETestSuite) TestFullDoubleSubmitFlow() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Phase 1: Client fetches CSRF token via GET
getResp, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer getResp.Body.Close()
s.Require().Equal(http.StatusOK, getResp.StatusCode)
var csrfCookie *http.Cookie
for _, c := range getResp.Cookies() {
if c.Name == "_gochat_csrf" {
csrfCookie = c
break
}
}
s.Require().NotNil(csrfCookie)
// Phase 2: Client echoes cookie value in header for the state-changing request
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.Header.Set("X-CSRF-Token", csrfCookie.Value)
postReq.AddCookie(csrfCookie)
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusOK, postResp.StatusCode,
"Full double-submit flow: GET→cookie→POST(header=cookie) should succeed")
bodyData := parseJSONBody(postResp.Body)
assert.Equal(s.T(), "ok", bodyData["status"],
"POST handler should return status=ok when CSRF validated")
}
// Test that each GET request generates a NEW token (no caching/reuse of old tokens)
func (s *CSRFE2ETestSuite) TestGET_GeneratesNewTokenEachRequest() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// First GET
resp1, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer resp1.Body.Close()
var cookie1 *http.Cookie
for _, c := range resp1.Cookies() {
if c.Name == "_gochat_csrf" {
cookie1 = c
break
}
}
s.Require().NotNil(cookie1)
// Second GET
resp2, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer resp2.Body.Close()
var cookie2 *http.Cookie
for _, c := range resp2.Cookies() {
if c.Name == "_gochat_csrf" {
cookie2 = c
break
}
}
s.Require().NotNil(cookie2)
// Tokens should differ (random generation)
assert.NotEqual(s.T(), cookie1.Value, cookie2.Value,
"Each GET request should generate a fresh CSRF token")
}
// Test that an old cookie token cannot be used after a new GET refreshes it
func (s *CSRFE2ETestSuite) TestStaleToken_AfterRefresh_Returns403() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = false
server := s.buildServer(cfg)
defer server.Close()
// Phase 1: Get first token
resp1, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer resp1.Body.Close()
var oldCookie *http.Cookie
for _, c := range resp1.Cookies() {
if c.Name == "_gochat_csrf" {
oldCookie = c
break
}
}
s.Require().NotNil(oldCookie)
oldToken := oldCookie.Value
// Phase 2: Get a new token (refresh)
resp2, err := http.Get(server.URL + "/api/test")
s.Require().NoError(err)
defer resp2.Body.Close()
var newCookie *http.Cookie
for _, c := range resp2.Cookies() {
if c.Name == "_gochat_csrf" {
newCookie = c
break
}
}
s.Require().NotNil(newCookie)
// Phase 3: POST with old cookie + old header (both stale)
// The server uses the cookie from the current request, so if we send
// the old cookie value, it will still match the old header. But if
// we send the new cookie + old header, it should fail.
postReq, err := http.NewRequest("POST", server.URL+"/api/test", nil)
s.Require().NoError(err)
postReq.AddCookie(newCookie) // New cookie
postReq.Header.Set("X-CSRF-Token", oldToken) // Old header value
postResp, err := http.DefaultClient.Do(postReq)
s.Require().NoError(err)
defer postResp.Body.Close()
assert.Equal(s.T(), http.StatusForbidden, postResp.StatusCode,
"POST with mismatched tokens (new cookie, old header) should be rejected")
}
// Test CookieSecure flag is set when configured
func (s *CSRFE2ETestSuite) TestCookieSecureFlag() {
cfg := middleware.DefaultCSRFConfig()
cfg.Enabled = true
cfg.CookieSecure = true // Secure flag enabled
// Use httptest.Server directly on the router (no TLS) to check cookie flags
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.CSRF(cfg))
r.GET("/api/test", func(c *gin.Context) {
token, _ := c.Get("csrf_token")
c.JSON(200, gin.H{"csrf_token": token})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/test", nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
// Parse Set-Cookie header to verify Secure flag
setCookieHeader := w.Header().Get("Set-Cookie")
assert.Contains(s.T(), setCookieHeader, "Secure",
"Cookie should have Secure flag when CookieSecure=true")
}
// Test CookieHTTPOnly flag is set when configured
func (s *CSRFE2ETestSuite) TestCookieHTTPOnlyFlag() {
cfg := middleware.CSRFConfig{
Enabled: true,
Secret: "test-httponly-secret",
CookieName: "_gochat_csrf_httponly",
HeaderName: "X-CSRF-Token",
TokenLength: 32,
SafeMethods: []string{"GET", "HEAD", "OPTIONS"},
CookieSecure: false,
CookieHTTPOnly: true, // HttpOnly enabled (unusual for double-submit, but configurable)
CookieSameSite: "Strict",
CookiePath: "/",
ExpirySeconds: 3600,
}
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.CSRF(cfg))
r.GET("/api/test", func(c *gin.Context) {
token, _ := c.Get("csrf_token")
c.JSON(200, gin.H{"csrf_token": token})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/test", nil)
r.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
setCookieHeader := w.Header().Get("Set-Cookie")
assert.Contains(s.T(), setCookieHeader, "HttpOnly",
"Cookie should have HttpOnly flag when CookieHTTPOnly=true")
}
// ============================================================================
// Test Suite Runner
// ============================================================================
func TestCSRFE2ETestSuite(t *testing.T) {
suite.Run(t, new(CSRFE2ETestSuite))
}