Repository: pallets/werkzeug
Pull request #2855 (2024-02-27 21:04:25)

Support Cookie CHIPS
CHIPS, Cookies Having Independent Partitioned State, allows for cookies to be opted into partitioned storage which is especially useful for thrid party cookies.

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


hmoffatt: How do you use this without `Response.set_cookie` also being modified to add the new parameter?

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 186e8f58..ca857171 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,5 +1,13 @@
 .. currentmodule:: werkzeug
 
+Version 3.1.0
+-------------
+
+Unreleased
+
+-  Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`
+
+
 Version 3.0.1
 -------------
 
diff --git a/src/werkzeug/http.py b/src/werkzeug/http.py
index 8280f51f..4ead0165 100644
--- a/src/werkzeug/http.py
+++ b/src/werkzeug/http.py
@@ -1216,6 +1216,7 @@ def dump_cookie(
     sync_expires: bool = True,
     max_size: int = 4093,
     samesite: str | None = None,
+    partitioned: bool = False,
 ) -> str:
     """Create a Set-Cookie header without the ``Set-Cookie`` prefix.
 
@@ -1252,9 +1253,14 @@ def dump_cookie(
         <cookie_>`_. Set to 0 to disable this check.
     :param samesite: Limits the scope of the cookie such that it will
         only be attached to requests if those requests are same-site.
+    :param partitioned: Opts the cookie into partitioned storage. This
+        will also set secure to True
 
     .. _`cookie`: http://browsercookielimits.squawky.net/
 
+    .. versionchanged:: 3.1
+        The ``partitioned`` parameter was added.
+
     .. versionchanged:: 3.0
         Passing bytes, and the ``charset`` parameter, were removed.
 
@@ -1298,6 +1304,9 @@ def dump_cookie(
         if samesite not in {"Strict", "Lax", "None"}:
             raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.")
 
+    if partitioned:
+        secure = True
+
     # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with
     # three octal digits, which matches http.cookies, although the RFC suggests base64.
     if not _cookie_no_quote_re.fullmatch(value):
@@ -1319,6 +1328,7 @@ def dump_cookie(
         ("HttpOnly", httponly),
         ("Path", path),
         ("SameSite", samesite),
+        ("Partitioned", partitioned),
     ):
         if v is None or v is False:
             continue
diff --git a/src/werkzeug/sansio/response.py b/src/werkzeug/sansio/response.py
index 271974ec..cfad0994 100644
--- a/src/werkzeug/sansio/response.py
+++ b/src/werkzeug/sansio/response.py
@@ -194,6 +194,7 @@ class Response:
         secure: bool = False,
         httponly: bool = False,
         samesite: str | None = None,
+        partitioned: bool = False,
     ) -> None:
         """Sets a cookie.
 
@@ -218,6 +219,7 @@ class Response:
         :param httponly: Disallow JavaScript access to the cookie.
         :param samesite: Limit the scope of the cookie to only be
             attached to requests that are "same-site".
+        :param partitioned: If ``True``, the cookie will be partitioned.
         """
         self.headers.add(
             "Set-Cookie",
@@ -232,6 +234,7 @@ class Response:
                 httponly=httponly,
                 max_size=self.max_cookie_size,
                 samesite=samesite,
+                partitioned=partitioned,
             ),
         )
 
@@ -243,6 +246,7 @@ class Response:
         secure: bool = False,
         httponly: bool = False,
         samesite: str | None = None,
+        partitioned: bool = False,
     ) -> None:
         """Delete a cookie.  Fails silently if key doesn't exist.
 
@@ -256,6 +260,7 @@ class Response:
         :param httponly: Disallow JavaScript access to the cookie.
         :param samesite: Limit the scope of the cookie to only be
             attached to requests that are "same-site".
+        :param partitioned: If ``True``, the cookie will be partitioned.
         """
         self.set_cookie(
             key,
@@ -266,6 +271,7 @@ class Response:
             secure=secure,
             httponly=httponly,
             samesite=samesite,
+            partitioned=partitioned,
         )
 
     @property


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
index bbd51ba3..1cf1613d 100644
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -576,6 +576,14 @@ class TestHTTPUtility:
         with pytest.raises(ValueError):
             http.dump_cookie("foo", "bar", samesite="invalid")
 
+    def test_cookie_partitioned(self):
+        value = http.dump_cookie("foo", "bar", partitioned=True, secure=True)
+        assert value == "foo=bar; Secure; Path=/; Partitioned"
+
+    def test_cookie_partitioned_sets_secure(self):
+        value = http.dump_cookie("foo", "bar", partitioned=True, secure=False)
+        assert value == "foo=bar; Secure; Path=/; Partitioned"
+
 
 class TestRange:
     def test_if_range_parsing(self):