Repository: thearchitector/starlette-securecookies (language: python)

Issue:
 starlette.middleware.sessions support?
Hi @thearchitector,

Thank you for this awesome lib!

Should this work with starlette.middleware.sessions? I have noticed the cookies are not encrypted:

```python
app = FastAPI()
app.add_middleware(
    SecureCookiesMiddleware,
    secrets=env.cookie_secrets,
    included_cookies=env.cookie_allowed_list,
)
app.add_middleware(SessionMiddleware, secret_key=env.session_secret_key)
```


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 6231bad..7c09ada 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ __pycache__
 .ruff_cache
 
 .venv
-.pdm.toml
+.pdm-python
 
 build
 dist
diff --git a/README.md b/README.md
index 40e8f22..c4838f1 100644
--- a/README.md
+++ b/README.md
@@ -56,13 +56,16 @@ from securecookies import SecureCookiesMiddleware
 
 middleware = [
     Middleware(
-        SecureCookiesMiddleware, secrets=["SUPER SECRET SECRET"]
+        SecureCookiesMiddleware, secrets=["SUPER SECRET SECRET"],
+        # your other middlewares
     )
 ]
 
 app = Starlette(routes=..., middleware=middleware)
 ```
 
+Note that if you're using another middleware that injects cookies into the response (such as SessionMiddleware), you have to make sure `SecureCookiesMiddleware` executes _after_ it so the cookie is present at encryption-time. Counter intuitively, in practice this means ensuring `SecureCookiesMiddleware` is _first_ in the list of middlewares.
+
 ## License
 
 This software is licensed under the [BSD 3-Clause License](LICENSE).
diff --git a/pdm.lock b/pdm.lock
index b57bd4e..2a6b65d 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -123,6 +123,12 @@ version = "2.0.0"
 requires_python = ">=3.7"
 summary = "brain-dead simple config-ini parsing"
 
+[[package]]
+name = "itsdangerous"
+version = "2.1.2"
+requires_python = ">=3.7"
+summary = "Safely pass data to untrusted environments and back."
+
 [[package]]
 name = "jinja2"
 version = "3.1.2"
@@ -273,8 +279,9 @@ requires_python = ">=3.7"
 summary = "Backport of pathlib-compatible object wrapper for zip files"
 
 [metadata]
-lock_version = "4.1"
-content_hash = "sha256:603af63ab4ccd4df91227d64152bcb2e66a097c759f7a2ad4b91452cb07fa9f9"
+lock_version = "4.2"
+groups = ["default", "dev"]
+content_hash = "sha256:b6a6a8c5dddf243e42c8ce93a5307a0a1b2f856dbcfdaf8b9d8fb762c1f85296"
 
 [metadata.files]
 "anyio 3.6.2" = [
@@ -493,6 +500,10 @@ content_hash = "sha256:603af63ab4ccd4df91227d64152bcb2e66a097c759f7a2ad4b91452cb
     {url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
     {url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
 ]
+"itsdangerous 2.1.2" = [
+    {url = "https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
+    {url = "https://files.pythonhosted.org/packages/7f/a1/d3fb83e7a61fa0c0d3d08ad0a94ddbeff3731c05212617dff3a94e097f08/itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
+]
 "jinja2 3.1.2" = [
     {url = "https://files.pythonhosted.org/packages/7a/ff/75c28576a1d900e87eb6335b063fab47a8ef3c8b4d88524c4bf78f670cce/Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
     {url = "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
diff --git a/pyproject.toml b/pyproject.toml
index cdb43bb..d126ba3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,7 @@ dev = [
     "pdoc~=13.0.1",
     "requests~=2.28.2",
     "httpx>=0.23.3",
+    "itsdangerous>=2.1.2",
 ]
 
 [tool.pdm.scripts]


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 83edc58..bdb69c0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,7 @@
 import pytest
 from cryptography.fernet import Fernet
 from starlette.applications import Starlette
-from starlette.middleware import Middleware
+from starlette.middleware import Middleware, sessions
 from starlette.responses import PlainTextResponse
 from starlette.routing import Route
 from starlette.testclient import TestClient
@@ -37,6 +37,20 @@ async def _validate(request):
     return response
 
 
+async def _session(request):
+    # set a session variable, expect SessionMiddleware to encode & sign it
+    # then SecureCookies to encrypt it
+    request.session["banana"] = "hello"
+    return PlainTextResponse("Session set")
+
+
+async def _validate_session(request):
+    # read a session variable, expect SecureCookies to decrypt it
+    # then SessionMiddleware to unsign & decode it
+    assert request.session["banana"] == "hello"
+    return PlainTextResponse("Session get")
+
+
 @pytest.fixture(scope="session")
 def secret():
     yield Fernet.generate_key()
@@ -65,9 +79,17 @@ def _func(cookies=None, **kwargs):
                     Route("/get", _get, methods=["GET"]),
                     Route("/getm", _get_multi, methods=["GET"]),
                     Route("/validate", _validate, methods=["GET"]),
+                    # third party
+                    Route("/session", _session, methods=["GET"]),
+                    Route("/session_val", _validate_session, methods=["GET"]),
                 ],
                 middleware=[
-                    Middleware(SecureCookiesMiddleware, secrets=[secret], **kwargs)
+                    # secure cookies must be first in the list so it can decrypt first
+                    # and encrypt last
+                    Middleware(SecureCookiesMiddleware, secrets=[secret], **kwargs),
+                    Middleware(
+                        sessions.SessionMiddleware, secret_key="verysecretsecret"
+                    ),
                 ],
             ),
             cookies=cookies,
diff --git a/tests/test_other-party.py b/tests/test_other-party.py
new file mode 100644
index 0000000..c358185
--- /dev/null
+++ b/tests/test_other-party.py
@@ -0,0 +1,18 @@
+import pytest
+from itsdangerous import BadSignature, TimestampSigner
+
+
+def test_third_party(client_factory, fernet):
+    # cookies added by other middlewares should get (de/en)crypted correctly
+    ## set a session cookie
+    response = client_factory().get("/session")
+    assert response.status_code == 200
+
+    ### it should be encrypted / will have an invalid sig
+    scookie = response.cookies["session"]
+    with pytest.raises(BadSignature):
+        TimestampSigner("verysecretsecret").unsign(scookie.encode())
+    TimestampSigner("verysecretsecret").unsign(fernet.decrypt(scookie.encode()))
+
+    ## read a session cookie, should be decrypted / will have a valid sig
+    client_factory(cookies={"session": scookie}).get("/session_val")