Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,9 +308,9 @@ const (
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7
const (
CookieSameSiteDisabled = "disabled" // not in RFC, just control "SameSite" attribute will not be set.
CookieSameSiteLaxMode = "lax"
CookieSameSiteStrictMode = "strict"
CookieSameSiteNoneMode = "none"
CookieSameSiteLaxMode = "Lax"
CookieSameSiteStrictMode = "Strict"
CookieSameSiteNoneMode = "None"
)

// Route Constraints
Expand Down
12 changes: 7 additions & 5 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,14 +432,16 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) {

var sameSite http.SameSite

switch utils.ToLower(cookie.SameSite) {
case CookieSameSiteStrictMode:
switch {
case utils.EqualFold(cookie.SameSite, CookieSameSiteStrictMode):
sameSite = http.SameSiteStrictMode
case CookieSameSiteNoneMode:
case utils.EqualFold(cookie.SameSite, CookieSameSiteNoneMode):
sameSite = http.SameSiteNoneMode
case CookieSameSiteDisabled:
// SameSite=None requires Secure=true per RFC and browser requirements
cookie.Secure = true
case utils.EqualFold(cookie.SameSite, CookieSameSiteDisabled):
sameSite = 0
case CookieSameSiteLaxMode:
case utils.EqualFold(cookie.SameSite, CookieSameSiteLaxMode):
sameSite = http.SameSiteLaxMode
default:
sameSite = http.SameSiteLaxMode
Expand Down
124 changes: 124 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,130 @@
)
}

// go test -run Test_Ctx_Cookie_SameSite_CaseInsensitive
func Test_Ctx_Cookie_SameSite_CaseInsensitive(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

tests := []struct {
name string
input string
expected string
}{
// Test case-insensitive Strict
{"Strict lowercase", "strict", "SameSite=Strict"},
{"Strict uppercase", "STRICT", "SameSite=Strict"},
{"Strict mixed case", "StRiCt", "SameSite=Strict"},
{"Strict proper case", "Strict", "SameSite=Strict"},

// Test case-insensitive Lax
{"Lax lowercase", "lax", "SameSite=Lax"},
{"Lax uppercase", "LAX", "SameSite=Lax"},
{"Lax mixed case", "LaX", "SameSite=Lax"},
{"Lax proper case", "Lax", "SameSite=Lax"},

// Test case-insensitive None
{"None lowercase", "none", "SameSite=None"},
{"None uppercase", "NONE", "SameSite=None"},
{"None mixed case", "NoNe", "SameSite=None"},
{"None proper case", "None", "SameSite=None"},

// Test case-insensitive disabled
{"Disabled lowercase", "disabled", ""},
{"Disabled uppercase", "DISABLED", ""},
{"Disabled mixed case", "DiSaBlEd", ""},
{"Disabled proper case", "disabled", ""},

// Test invalid values default to Lax
{"Invalid value", "invalid", "SameSite=Lax"},
{"Empty value", "", "SameSite=Lax"},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
// Reset response
c.Response().Reset()

cookie := &Cookie{
Name: "test",
Value: "value",
SameSite: tc.input,
}
c.Res().Cookie(cookie)

setCookieHeader := c.Res().Get(HeaderSetCookie)
if tc.expected == "" {
// For disabled, SameSite should not appear in the header
require.NotContains(t, setCookieHeader, "SameSite")
} else {
// For all other cases, the expected SameSite should appear
require.Contains(t, setCookieHeader, tc.expected)
}
})
}
}

// go test -run Test_Ctx_Cookie_SameSite_None_Secure
func Test_Ctx_Cookie_SameSite_None_Secure(t *testing.T) {
t.Parallel()

tests := []struct {

Check failure on line 1269 in ctx_test.go

View workflow job for this annotation

GitHub Actions / lint

fieldalignment: struct with 48 pointer bytes could be 40 (govet)
name string
sameSite string
initialSecure bool
expectedSecure bool
expectedContain string
}{
// Test that SameSite=None automatically sets Secure=true
{"None lowercase - initially false", "none", false, true, "secure"},
{"None uppercase - initially false", "NONE", false, true, "secure"},
{"None mixed case - initially false", "NoNe", false, true, "secure"},
{"None proper case - initially false", "None", false, true, "secure"},

// Test that SameSite=None respects existing Secure=true
{"None with existing secure", "None", true, true, "secure"},

// Test that other SameSite values don't force Secure=true
{"Strict doesn't force secure", "Strict", false, false, "SameSite=Strict"},
{"Lax doesn't force secure", "Lax", false, false, "SameSite=Lax"},
{"Disabled doesn't force secure", "disabled", false, false, ""},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

// Create a fresh context for each test case
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})

cookie := &Cookie{
Name: "test",
Value: "value",
SameSite: tc.sameSite,
Secure: tc.initialSecure,
}
c.Res().Cookie(cookie)

// Check that the cookie's Secure field was updated as expected
require.Equal(t, tc.expectedSecure, cookie.Secure, "Cookie.Secure should be %v", tc.expectedSecure)

setCookieHeader := c.Res().Get(HeaderSetCookie)
if tc.expectedContain != "" {
require.Contains(t, setCookieHeader, tc.expectedContain)
}

// Verify that SameSite=None always includes secure in the header
if tc.sameSite == "none" || tc.sameSite == "NONE" || tc.sameSite == "NoNe" || tc.sameSite == "None" {
require.Contains(t, setCookieHeader, "secure")
require.Contains(t, setCookieHeader, "SameSite=None")
}
})
}
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
func Benchmark_Ctx_Cookie(b *testing.B) {
app := New()
Expand Down
10 changes: 10 additions & 0 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,16 @@ app.Get("/", func(c fiber.Ctx) error {
})
```

:::info
When setting a cookie with `SameSite=None`, Fiber automatically sets `Secure=true` as required by RFC 6265bis and modern browsers. This ensures compliance with the "None" SameSite policy which mandates that cookies must be sent over secure connections.

For more information, see:

- [Mozilla Documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#none)
- [Chrome Documentation](https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure)

:::

:::info
Partitioned cookies allow partitioning the cookie jar by top-level site, enhancing user privacy by preventing cookies from being shared across different sites. This feature is particularly useful in scenarios where a user interacts with embedded third-party services that should not have access to the main site's cookies. You can check out [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) for more information.
:::
Expand Down
1 change: 1 addition & 0 deletions docs/whats_new.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ testConfig := fiber.TestConfig{
### New Features

- Cookie now allows Partitioned cookies for [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) support. CHIPS (Cookies Having Independent Partitioned State) is a feature that improves privacy by allowing cookies to be partitioned by top-level site, mitigating cross-site tracking.
- Cookie automatic security enforcement: When setting a cookie with `SameSite=None`, Fiber automatically sets `Secure=true` as required by RFC 6265bis and modern browsers (Chrome, Firefox, Safari). This ensures compliance with the "None" SameSite policy. See [Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#none) and [Chrome docs](https://developers.google.com/search/blog/2020/01/get-ready-for-new-samesitenone-secure) for details.
- Context now implements [context.Context](https://pkg.go.dev/context#Context).

### New Methods
Expand Down
Loading