Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion auth/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (auth *Authenticator) GetSession(sessionID string) (*LoginSession, error) {
return &session, nil
}

func (auth *Authenticator) MakeSessionCookie(session *LoginSession, secureCookie bool, httpOnly bool) *http.Cookie {
func (auth *Authenticator) MakeSessionCookie(session *LoginSession, secureCookie bool, httpOnly bool, sameSite http.SameSite) *http.Cookie {
if session == nil {
return nil
}
Expand All @@ -145,6 +145,7 @@ func (auth *Authenticator) MakeSessionCookie(session *LoginSession, secureCookie
Expires: session.Expiration,
Secure: secureCookie,
HttpOnly: httpOnly,
SameSite: sameSite,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to go verify that http.Cookie doesn't emit the SameSite attribute when SameSiteDefaultMode is specified, to ensure we won't see any functional change for users not using CORS or setting the flag.
It's possible that's worth mentioning the default handling in a function description comment, to make that clear. (on the fence about the necessity of that, take it or leave it)

}
}

Expand Down
18 changes: 9 additions & 9 deletions auth/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ func TestMakeSessionCookie(t *testing.T) {
Ttl: 24 * time.Hour,
}

cookie := auth.MakeSessionCookie(mockSession, false, false)
cookie := auth.MakeSessionCookie(mockSession, false, false, http.SameSiteDefaultMode)
assert.Equal(t, DefaultCookieName, cookie.Name)
assert.Equal(t, sessionID, cookie.Value)
assert.NotEmpty(t, cookie.Expires)

// Cookies should not be created with uninitialized session
mockSession = nil
cookie = auth.MakeSessionCookie(mockSession, false, false)
cookie = auth.MakeSessionCookie(mockSession, false, false, http.SameSiteDefaultMode)
assert.Empty(t, cookie)
}

Expand All @@ -143,16 +143,16 @@ func TestMakeSessionCookieProperties(t *testing.T) {
Ttl: 24 * time.Hour,
}

unsecuredCookie := auth.MakeSessionCookie(mockSession, false, false)
unsecuredCookie := auth.MakeSessionCookie(mockSession, false, false, http.SameSiteDefaultMode)
assert.False(t, unsecuredCookie.Secure)

securedCookie := auth.MakeSessionCookie(mockSession, true, false)
securedCookie := auth.MakeSessionCookie(mockSession, true, false, http.SameSiteDefaultMode)
assert.True(t, securedCookie.Secure)

httpOnlyFalseCookie := auth.MakeSessionCookie(mockSession, false, false)
httpOnlyFalseCookie := auth.MakeSessionCookie(mockSession, false, false, http.SameSiteDefaultMode)
assert.False(t, httpOnlyFalseCookie.HttpOnly)

httpOnlyCookie := auth.MakeSessionCookie(mockSession, false, true)
httpOnlyCookie := auth.MakeSessionCookie(mockSession, false, true, http.SameSiteDefaultMode)
assert.True(t, httpOnlyCookie.HttpOnly)
}

Expand Down Expand Up @@ -248,7 +248,7 @@ func TestCreateSessionChangePassword(t *testing.T) {

request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
request.AddCookie(auth.MakeSessionCookie(session, true, true))
request.AddCookie(auth.MakeSessionCookie(session, true, true, http.SameSiteDefaultMode))

recorder := httptest.NewRecorder()
_, err = auth.AuthenticateCookie(request, recorder)
Expand Down Expand Up @@ -304,7 +304,7 @@ func TestUserWithoutSessionUUID(t *testing.T) {

request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
request.AddCookie(auth.MakeSessionCookie(session, true, true))
request.AddCookie(auth.MakeSessionCookie(session, true, true, http.SameSiteDefaultMode))

recorder := httptest.NewRecorder()
_, err = auth.AuthenticateCookie(request, recorder)
Expand Down Expand Up @@ -334,7 +334,7 @@ func TestUserDeleteAllSessions(t *testing.T) {

request, err := http.NewRequest(http.MethodGet, "", nil)
require.NoError(t, err)
request.AddCookie(auth.MakeSessionCookie(session, true, true))
request.AddCookie(auth.MakeSessionCookie(session, true, true, http.SameSiteDefaultMode))
recorder := httptest.NewRecorder()

_, err = auth.AuthenticateCookie(request, recorder)
Expand Down
1 change: 1 addition & 0 deletions rest/cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ func TestCORSLoginOriginPerDatabase(t *testing.T) {
cookie, err := http.ParseSetCookie(resp.Header().Get("Set-Cookie"))
require.NoError(t, err)
require.NotEmpty(t, cookie.Path)
require.Equal(t, http.SameSiteNoneMode, cookie.SameSite)
reqHeaders["Cookie"] = fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
}
resp = rt.SendRequestWithHeaders(http.MethodDelete, "/{{.db}}/_session", "", reqHeaders)
Expand Down
6 changes: 5 additions & 1 deletion rest/session_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,11 @@ func (h *handler) makeSessionWithTTL(user auth.User, expiry time.Duration) (sess
if err != nil {
return "", err
}
cookie := auth.MakeSessionCookie(session, h.db.Options.SecureCookieOverride, h.db.Options.SessionCookieHttpOnly)
sameSite := http.SameSiteDefaultMode
if !h.getCORSConfig().IsEmpty() {
sameSite = http.SameSiteNoneMode
}
cookie := auth.MakeSessionCookie(session, h.db.Options.SecureCookieOverride, h.db.Options.SessionCookieHttpOnly, sameSite)
Copy link

Copilot AI Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When setting SameSite=None, the Secure flag must also be set to true for the cookie to be accepted by browsers. The current implementation doesn't enforce this constraint. Consider adding validation to ensure SecureCookieOverride is true when CORS is enabled, or document this requirement.

Suggested change
if !h.getCORSConfig().IsEmpty() {
sameSite = http.SameSiteNoneMode
}
cookie := auth.MakeSessionCookie(session, h.db.Options.SecureCookieOverride, h.db.Options.SessionCookieHttpOnly, sameSite)
secure := h.db.Options.SecureCookieOverride
if !h.getCORSConfig().IsEmpty() {
sameSite = http.SameSiteNoneMode
// When SameSite=None, Secure must be true for browser compatibility.
secure = true
}
cookie := auth.MakeSessionCookie(session, secure, h.db.Options.SessionCookieHttpOnly, sameSite)

Copilot uses AI. Check for mistakes.
base.AddDbPathToCookie(h.rq, cookie)
http.SetCookie(h.response, cookie)
return session.ID, nil
Expand Down
18 changes: 18 additions & 0 deletions rest/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ func TestCORSLoginOriginOnSessionPost(t *testing.T) {

response = rt.SendRequestWithHeaders("POST", "/db/_facebook", `{"access_token":"true"}`, reqHeaders)
assertGatewayStatus(t, response, 401)

const username = "alice"
rt.CreateUser(username, []string{"*"})

response = rt.SendUserRequest(http.MethodPost, "/{{.db}}/_session", "", username)
RequireStatus(t, response, http.StatusOK)
cookie, err := http.ParseSetCookie(response.Header().Get("Set-Cookie"))
require.NoError(t, err)
require.Equal(t, cookie.SameSite, http.SameSiteNoneMode)
}

// #issue 991
Expand All @@ -53,6 +62,15 @@ func TestCORSLoginOriginOnSessionPostNoCORSConfig(t *testing.T) {

response := rt.SendRequestWithHeaders("POST", "/db/_session", `{"name":"jchris","password":"secret"}`, reqHeaders)
RequireStatus(t, response, 400)

const username = "alice"
rt.CreateUser(username, []string{"*"})

response = rt.SendUserRequest(http.MethodPost, "/{{.db}}/_session", "", username)
RequireStatus(t, response, http.StatusOK)
cookie, err := http.ParseSetCookie(response.Header().Get("Set-Cookie"))
require.NoError(t, err)
require.NotContains(t, cookie.String(), "SameSite")
}

func TestNoCORSOriginOnSessionPost(t *testing.T) {
Expand Down