diff --git a/token/jwt.go b/token/jwt.go index 0a6649e9..bcb1912b 100644 --- a/token/jwt.go +++ b/token/jwt.go @@ -362,7 +362,8 @@ func (j *Service) IsExpired(claims Claims) bool { // Reset token's cookies func (j *Service) Reset(w http.ResponseWriter) { - jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: "", HttpOnly: false, Path: "/", Domain: j.JWTCookieDomain, //nolint:gosec // expired removal cookie, carries no value + // HttpOnly as on the live cookie: a browser rejects an expiry for a __Http- prefixed name without it + jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: "", HttpOnly: true, Path: "/", Domain: j.JWTCookieDomain, //nolint:gosec // Secure and SameSite come from service config MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies, SameSite: j.SameSite, Partitioned: j.PartitionedCookies} http.SetCookie(w, &jwtCookie) diff --git a/token/jwt_test.go b/token/jwt_test.go index 7e79ecd9..804f1099 100644 --- a/token/jwt_test.go +++ b/token/jwt_test.go @@ -280,15 +280,15 @@ func TestJWT_CookieAttributesFromOpts(t *testing.T) { t.Run("reset", func(t *testing.T) { // the removal pair has to repeat these attributes, though not because they are part of - // the cookie's identity: that is name, domain and path alone, which is why Reset writing - // HttpOnly false over a cookie Set wrote HttpOnly true still replaces it. The reason is - // that Chrome and Firefox reject SameSite=None without Secure outright, and a removal - // cookie the browser discards leaves the original in place + // the cookie's identity: that is name, domain and path alone. The reason is that Chrome and + // Firefox reject SameSite=None without Secure outright, and a removal cookie the browser + // discards leaves the original in place rr := httptest.NewRecorder() j.Reset(rr) cookies := rr.Result().Cookies() checkPair(t, cookies) + assert.True(t, cookies[0].HttpOnly, "a browser rejects a __Http- prefixed removal cookie without HttpOnly") for _, c := range cookies { assert.Empty(t, c.Value, "%s still carries a value", c.Name) assert.Equal(t, -1, c.MaxAge, "%s does not expire", c.Name) @@ -645,7 +645,7 @@ func TestJWT_Reset(t *testing.T) { require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) - assert.Equal(t, "jc1=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0", resp.Header.Get("Set-Cookie")) + assert.Equal(t, "jc1=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; HttpOnly", resp.Header.Get("Set-Cookie")) assert.Equal(t, "0", resp.Header.Get("Content-Length")) } diff --git a/v2/token/jwt.go b/v2/token/jwt.go index 7958e1c8..03afd2b2 100644 --- a/v2/token/jwt.go +++ b/v2/token/jwt.go @@ -384,7 +384,8 @@ func (j *Service) IsExpired(claims Claims) bool { // Reset token's cookies func (j *Service) Reset(w http.ResponseWriter) { - jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: "", HttpOnly: false, Path: "/", Domain: j.JWTCookieDomain, //nolint:gosec // expired removal cookie, carries no value + // HttpOnly as on the live cookie: a browser rejects an expiry for a __Http- prefixed name without it + jwtCookie := http.Cookie{Name: j.JWTCookieName, Value: "", HttpOnly: true, Path: "/", Domain: j.JWTCookieDomain, //nolint:gosec // Secure and SameSite come from service config MaxAge: -1, Expires: time.Unix(0, 0), Secure: j.SecureCookies, SameSite: j.SameSite, Partitioned: j.PartitionedCookies} http.SetCookie(w, &jwtCookie) diff --git a/v2/token/jwt_test.go b/v2/token/jwt_test.go index d975c34f..3473c823 100644 --- a/v2/token/jwt_test.go +++ b/v2/token/jwt_test.go @@ -280,15 +280,15 @@ func TestJWT_CookieAttributesFromOpts(t *testing.T) { t.Run("reset", func(t *testing.T) { // the removal pair has to repeat these attributes, though not because they are part of - // the cookie's identity: that is name, domain and path alone, which is why Reset writing - // HttpOnly false over a cookie Set wrote HttpOnly true still replaces it. The reason is - // that Chrome and Firefox reject SameSite=None without Secure outright, and a removal - // cookie the browser discards leaves the original in place + // the cookie's identity: that is name, domain and path alone. The reason is that Chrome and + // Firefox reject SameSite=None without Secure outright, and a removal cookie the browser + // discards leaves the original in place rr := httptest.NewRecorder() j.Reset(rr) cookies := rr.Result().Cookies() checkPair(t, cookies) + assert.True(t, cookies[0].HttpOnly, "a browser rejects a __Http- prefixed removal cookie without HttpOnly") for _, c := range cookies { assert.Empty(t, c.Value, "%s still carries a value", c.Name) assert.Equal(t, -1, c.MaxAge, "%s does not expire", c.Name) @@ -645,7 +645,7 @@ func TestJWT_Reset(t *testing.T) { require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) - assert.Equal(t, "jc1=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0", resp.Header.Get("Set-Cookie")) + assert.Equal(t, "jc1=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; HttpOnly", resp.Header.Get("Set-Cookie")) assert.Equal(t, "0", resp.Header.Get("Content-Length")) }