Repository: litestar-org/litestar

Issue:
refactor: improve typing in CSRF middleware
https://github.com/litestar-org/litestar/blob/7414f7fd7d4782223502895e6a23b77ed635cd2d/litestar/middleware/csrf.py#L87-L127

At line 105, we use `dict.get()` to set the value of `existing_csrf_token` so it can be `None` if the header doesn't exist.

At line 123, that block is guarded by `self._csrf_tokens_match()` which will return `False` if it is `None`, so actually `existing_csrf_token` cannot be falsy in this block, its just that its value is not narrowed appropriately.

Fixing this would probably be as simple as using `request.cookies.get(..., "")` and `request.headers.get(..., "")` on lines 104 and 105 respectively, and re-type downstream methods to only accept `str` instead of `str | None`.

_Originally posted by @peterschutt in https://github.com/litestar-org/litestar/pull/2751#discussion_r1405515256_
            

<!-- POLAR PLEDGE BADGE START -->
---
> [!NOTE]  
> While we are open for sponsoring on [GitHub Sponsors](https://github.com/sponsors/litestar-org/) and 
> [OpenCollective](https://opencollective.com/litestar), we also utilize [Polar.sh](https://polar.sh/) to engage in pledge-based sponsorship.
>
> Check out all issues funded or available for funding [on our Polar.sh Litestar dashboard](https://polar.sh/litestar-org)
> * If you would like to see an issue prioritized, make a pledge towards it!
> * We receive the pledge once the issue is completed & verified
> * This, along with engagement in the community, helps us know which features are a priority to our users.

<a href="https://polar.sh/litestar-org/litestar/issues/2770">
<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://polar.sh/api/github/litestar-org/litestar/issues/2770/pledge.svg?darkmode=1">
  <img alt="Fund with Polar" src="https://polar.sh/api/github/litestar-org/litestar/issues/2770/pledge.svg">
</picture>
</a>
<!-- POLAR PLEDGE BADGE END -->



Discussion:

Resolving patch:
diff --git a/litestar/middleware/csrf.py b/litestar/middleware/csrf.py
--- a/litestar/middleware/csrf.py
+++ b/litestar/middleware/csrf.py
@@ -119,9 +119,12 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
         ):
             token = connection_state.csrf_token = csrf_cookie or generate_csrf_token(secret=self.config.secret)
             await self.app(scope, receive, self.create_send_wrapper(send=send, csrf_cookie=csrf_cookie, token=token))
-        elif self._csrf_tokens_match(existing_csrf_token, csrf_cookie):
-            # we haven't properly narrowed the type of `existing_csrf_token` to be non-None, but we know it is
-            connection_state.csrf_token = existing_csrf_token  # type: ignore[assignment]
+        elif (
+            existing_csrf_token is not None
+            and csrf_cookie is not None
+            and self._csrf_tokens_match(existing_csrf_token, csrf_cookie)
+        ):
+            connection_state.csrf_token = existing_csrf_token
             await self.app(scope, receive, send)
         else:
             raise PermissionDeniedException("CSRF token verification failed")
@@ -177,11 +180,8 @@ def _decode_csrf_token(self, token: str) -> str | None:
         expected_hash = generate_csrf_hash(token=token_secret, secret=self.config.secret)
         return token_secret if compare_digest(existing_hash, expected_hash) else None
 
-    def _csrf_tokens_match(self, request_csrf_token: str | None, cookie_csrf_token: str | None) -> bool:
+    def _csrf_tokens_match(self, request_csrf_token: str, cookie_csrf_token: str) -> bool:
         """Take the CSRF tokens from the request and the cookie and verify both are valid and identical."""
-        if not (request_csrf_token and cookie_csrf_token):
-            return False
-
         decoded_request_token = self._decode_csrf_token(request_csrf_token)
         decoded_cookie_token = self._decode_csrf_token(cookie_csrf_token)
         if decoded_request_token is None or decoded_cookie_token is None:


Test patch: