Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -309,9 +309,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
159 changes: 159 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,165 @@ func Test_Ctx_Cookie_StrictPartitioned(t *testing.T) {
)
}

// 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()

testCases := []struct {
name string
cookie *Cookie
expectedInHeader string
shouldBeSecure bool
}{
{
name: "Empty value",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "",
},
expectedInHeader: "SameSite=Lax",
shouldBeSecure: false,
},
{
name: "None uppercase",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "None",
},
expectedInHeader: "SameSite=None",
shouldBeSecure: true,
},
{
name: "None lowercase",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "none",
},
expectedInHeader: "SameSite=None",
shouldBeSecure: true,
},
{
name: "Lax proper case",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "Lax",
},
expectedInHeader: "SameSite=Lax",
shouldBeSecure: false,
},
{
name: "Strict uppercase",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "STRICT",
},
expectedInHeader: "SameSite=Strict",
shouldBeSecure: false,
},
{
name: "Disabled Secure",
cookie: &Cookie{
Name: "test",
Value: "value",
SameSite: "none",
Secure: false,
},
expectedInHeader: "SameSite=None",
shouldBeSecure: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
app := New()
ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
defer app.ReleaseCtx(ctx)

ctx.Cookie(tc.cookie)

cookie := string(ctx.Response().Header.PeekCookie(tc.cookie.Name))
require.Contains(t, cookie, tc.expectedInHeader)

if tc.shouldBeSecure {
require.Contains(t, cookie, "secure")
} else {
require.NotContains(t, cookie, "secure")
}
})
}
}

// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
func Benchmark_Ctx_Cookie(b *testing.B) {
app := New()
Expand Down
14 changes: 12 additions & 2 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,7 @@ app.Get("/set", func(c fiber.Ctx) error {
Value: "randomvalue",
Expires: time.Now().Add(24 * time.Hour),
HTTPOnly: true,
SameSite: "lax",
SameSite: "Lax",
})

// ...
Expand All @@ -1559,7 +1559,7 @@ app.Get("/delete", func(c fiber.Ctx) error {
// Set expiry date to the past
Expires: time.Now().Add(-(time.Hour * 2)),
HTTPOnly: true,
SameSite: "lax",
SameSite: "Lax",
})

// ...
Expand Down 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
53 changes: 29 additions & 24 deletions middleware/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ func (s *Session) setSession() {
s.ctx.Response().Header.SetBytesV(s.config.sessionName, []byte(s.id))
} else {
fcookie := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(fcookie)

fcookie.SetKey(s.config.sessionName)
fcookie.SetValue(s.id)
fcookie.SetPath(s.config.CookiePath)
Expand All @@ -388,19 +390,9 @@ func (s *Session) setSession() {
fcookie.SetMaxAge(int(s.idleTimeout.Seconds()))
fcookie.SetExpire(time.Now().Add(s.idleTimeout))
}
fcookie.SetSecure(s.config.CookieSecure)
fcookie.SetHTTPOnly(s.config.CookieHTTPOnly)

switch utils.ToLower(s.config.CookieSameSite) {
case "strict":
fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
case "none":
fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
default:
fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
}

s.setCookieAttributes(fcookie)
s.ctx.Response().Header.SetCookie(fcookie)
fasthttp.ReleaseCookie(fcookie)
}
}

Expand All @@ -417,28 +409,41 @@ func (s *Session) delSession() {
s.ctx.Response().Header.DelCookie(s.config.sessionName)

fcookie := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(fcookie)

fcookie.SetKey(s.config.sessionName)
fcookie.SetPath(s.config.CookiePath)
fcookie.SetDomain(s.config.CookieDomain)
fcookie.SetMaxAge(-1)
fcookie.SetExpire(time.Now().Add(-1 * time.Minute))
fcookie.SetSecure(s.config.CookieSecure)
fcookie.SetHTTPOnly(s.config.CookieHTTPOnly)

switch utils.ToLower(s.config.CookieSameSite) {
case "strict":
fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
case "none":
fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
default:
fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
}

s.setCookieAttributes(fcookie)
s.ctx.Response().Header.SetCookie(fcookie)
fasthttp.ReleaseCookie(fcookie)
}
}

// setCookieAttributes sets the cookie attributes based on the session config.
func (s *Session) setCookieAttributes(fcookie *fasthttp.Cookie) {
// Set SameSite attribute
switch {
case utils.EqualFold(s.config.CookieSameSite, fiber.CookieSameSiteStrictMode):
fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
case utils.EqualFold(s.config.CookieSameSite, fiber.CookieSameSiteNoneMode):
fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
default:
fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
}

// The Secure attribute is required for SameSite=None
if fcookie.SameSite() == fasthttp.CookieSameSiteNoneMode {
fcookie.SetSecure(true)
} else {
fcookie.SetSecure(s.config.CookieSecure)
}

fcookie.SetHTTPOnly(s.config.CookieHTTPOnly)
}

// decodeSessionData decodes session data from raw bytes
//
// Parameters:
Expand Down
Loading
Loading