Repository: litestar-org/litestar

Issue:
Bug: regression v2.0.0beta4 -> main@HEAD
### Description

There appears to be a regression between tag v2.0.0beta4 and the current (2.0.0rc1 PyPi release) (or if you prefer, also commit d257d099993179f1133a96441bf816e1de435007, which is the latest commit on main. This seems very likely related to #2160  which my guess would be only fixed the problem for JWT, not the SessionAuth middleware that I am using.

### URL to code causing the issue

https://github.com/Mattwmaster58/MacTrack/tree/broken-auth

### MCVE

```python
(working on this)
```


### Steps to reproduce

```bash
1. Clone repo, checkout `broken-auth` branch, poetry install, run, request to OPTIONS endpoint, observe:

`INFO:     127.0.0.1:55754 - "OPTIONS /api/filters/create HTTP/1.1" 401 Unauthorized`
```


### Screenshots

_No response_

### Logs

```bash
ERROR - 2023-08-19 18:27:03,266908605 - litestar - middleware - exception raised on http connection to route /api/filters/create

Traceback (most recent call last):
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/middleware/authentication.py", line 87, in __call__
    auth_result = await self.authenticate_request(ASGIConnection(scope))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/security/session_auth/middleware.py", line 111, in authenticate_request
    raise NotAuthorizedException("no session data found")
litestar.exceptions.http_exceptions.NotAuthorizedException: 401: no session data found
Traceback (most recent call last):
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/middleware/exceptions/middleware.py", line 191, in __call__
    await self.app(scope, receive, send)
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/middleware/authentication.py", line 87, in __call__
    auth_result = await self.authenticate_request(ASGIConnection(scope))
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/user/Documents/projects/mac-track/api/.venv/lib/python3.11/site-packages/litestar/security/session_auth/middleware.py", line 111, in authenticate_request
    raise NotAuthorizedException("no session data found")
litestar.exceptions.http_exceptions.NotAuthorizedException: 401: no session data found
INFO:     127.0.0.1:55754 - "OPTIONS /api/filters/create HTTP/1.1" 401 Unauthorized
```


### Litestar Version

2.0.0rc1 as well as d257d099993179f1133a96441bf816e1de435007

### Platform

- [X] Linux
- [ ] Mac
- [ ] Windows
- [ ] Other (Please specify in the description above)

<!-- POLAR PLEDGE BADGE START -->
---

## Funding
* If you would like to see an issue prioritized, make a pledge towards it!
* We receive the pledge once the issue is completed & verified

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

StaticFilesConfig and virtual directories
I'm trying to write a ``FileSystemProtocol`` to load files from the package data using [importlib_resources](https://importlib-resources.readthedocs.io/en/latest/using.html#). But because ``directories`` is defined as ``DirectoryPath``, pydantic checks if the given directories exist in the local filesystem. 

This is not generally true, especially in any kind of virtual filesystem (e.g. a zipped package). I think this condition should be relaxed to support virtual filesystems.

https://github.com/starlite-api/starlite/blob/9bb6dcd57c10a591377cf8e3a537e9292566d5b9/starlite/config/static_files.py#L32


Discussion:

I agree. Do you want to submit a PR?

Resolving patch:
diff --git a/litestar/security/session_auth/auth.py b/litestar/security/session_auth/auth.py
--- a/litestar/security/session_auth/auth.py
+++ b/litestar/security/session_auth/auth.py
@@ -1,7 +1,7 @@
 from __future__ import annotations
 
 from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Iterable
+from typing import TYPE_CHECKING, Any, Callable, Dict, Generic, Iterable, Sequence, cast
 
 from litestar.middleware.base import DefineMiddleware
 from litestar.middleware.session.base import BaseBackendConfig, BaseSessionBackendT
@@ -14,7 +14,7 @@
 if TYPE_CHECKING:
     from litestar.connection import ASGIConnection
     from litestar.di import Provide
-    from litestar.types import ControllerRouterHandler, Guard, Scopes, SyncOrAsyncUnion, TypeEncodersMap
+    from litestar.types import ControllerRouterHandler, Guard, Method, Scopes, SyncOrAsyncUnion, TypeEncodersMap
 
 
 @dataclass
@@ -46,6 +46,10 @@ class SessionAuth(Generic[UserType, BaseSessionBackendT], AbstractSecurityConfig
     """A pattern or list of patterns to skip in the authentication middleware."""
     exclude_opt_key: str = field(default="exclude_from_auth")
     """An identifier to use on routes to disable authentication and authorization checks for a particular route."""
+    exclude_http_methods: Sequence[Method] | None = field(
+        default_factory=lambda: cast("Sequence[Method]", ["OPTIONS", "HEAD"])
+    )
+    """A sequence of http methods that do not require authentication. Defaults to ['OPTIONS', 'HEAD']"""
     scopes: Scopes | None = field(default=None)
     """ASGI scopes processed by the authentication middleware, if ``None``, both ``http`` and ``websocket`` will be
     processed."""
diff --git a/litestar/security/session_auth/middleware.py b/litestar/security/session_auth/middleware.py
--- a/litestar/security/session_auth/middleware.py
+++ b/litestar/security/session_auth/middleware.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Sequence
 
 from litestar.exceptions import NotAuthorizedException
 from litestar.middleware.authentication import (
@@ -9,11 +9,10 @@
 )
 from litestar.middleware.exceptions import ExceptionHandlerMiddleware
 from litestar.middleware.session.base import SessionMiddleware
-from litestar.types import Empty, Scopes
+from litestar.types import Empty, Method, Scopes
 
 __all__ = ("MiddlewareWrapper", "SessionAuthMiddleware")
 
-
 if TYPE_CHECKING:
     from litestar.connection import ASGIConnection
     from litestar.security.session_auth.auth import SessionAuth
@@ -53,6 +52,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
             auth_middleware = self.config.authentication_middleware_class(
                 app=self.app,
                 exclude=self.config.exclude,
+                exclude_http_methods=self.config.exclude_http_methods,
                 exclude_opt_key=self.config.exclude_opt_key,
                 scopes=self.config.scopes,
                 retrieve_user_handler=self.config.retrieve_user_handler,  # type: ignore
@@ -77,20 +77,28 @@ def __init__(
         self,
         app: ASGIApp,
         exclude: str | list[str] | None,
+        exclude_http_methods: Sequence[Method] | None,
         exclude_opt_key: str,
-        scopes: Scopes | None,
         retrieve_user_handler: AsyncCallable[[dict[str, Any], ASGIConnection[Any, Any, Any, Any]], Any],
+        scopes: Scopes | None,
     ) -> None:
         """Session based authentication middleware.
 
         Args:
             app: An ASGIApp, this value is the next ASGI handler to call in the middleware stack.
             exclude: A pattern or list of patterns to skip in the authentication middleware.
+            exclude_http_methods: A sequence of http methods that do not require authentication.
             exclude_opt_key: An identifier to use on routes to disable authentication and authorization checks for a particular route.
             scopes: ASGI scopes processed by the authentication middleware.
             retrieve_user_handler: Callable that receives the ``session`` value from the authentication middleware and returns a ``user`` value.
         """
-        super().__init__(app=app, exclude=exclude, exclude_from_auth_key=exclude_opt_key, scopes=scopes)
+        super().__init__(
+            app=app,
+            exclude=exclude,
+            exclude_from_auth_key=exclude_opt_key,
+            exclude_http_methods=exclude_http_methods,
+            scopes=scopes,
+        )
         self.retrieve_user_handler = retrieve_user_handler
 
     async def authenticate_request(self, connection: ASGIConnection[Any, Any, Any, Any]) -> AuthenticationResult:


Test patch:
diff --git a/tests/unit/test_middleware/test_session/test_integration.py b/tests/unit/test_middleware/test_session/test_integration.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/test_middleware/test_session/test_integration.py
@@ -0,0 +1,70 @@
+from typing import Any, Dict, Literal, Optional
+from uuid import UUID
+
+from pydantic import BaseModel, EmailStr, SecretStr
+
+from litestar import Request, get, post
+from litestar.connection import ASGIConnection
+from litestar.exceptions import NotAuthorizedException
+from litestar.middleware.session.server_side import (
+    ServerSideSessionBackend,
+    ServerSideSessionConfig,
+)
+from litestar.security.session_auth import SessionAuth
+from litestar.status_codes import HTTP_204_NO_CONTENT
+from litestar.stores.memory import MemoryStore
+from litestar.testing import create_test_client
+
+
+class User(BaseModel):
+    id: UUID
+    name: str
+    email: EmailStr
+
+
+class UserLoginPayload(BaseModel):
+    email: EmailStr
+    password: SecretStr
+
+
+MOCK_DB: Dict[str, User] = {}
+memory_store = MemoryStore()
+
+
+async def retrieve_user_handler(
+    session: Dict[str, Any], connection: "ASGIConnection[Any, Any, Any, Any]"
+) -> Optional[User]:
+    return MOCK_DB.get(user_id) if (user_id := session.get("user_id")) else None
+
+
+@post("/login")
+async def login(data: UserLoginPayload, request: "Request[Any, Any, Any]") -> User:
+    user_id_bytes = await memory_store.get(data.email)
+
+    if not user_id_bytes:
+        raise NotAuthorizedException
+
+    user_id = user_id_bytes.decode("utf-8")
+    request.set_session({"user_id": user_id})
+    return MOCK_DB[user_id]
+
+
+@get("/user", sync_to_thread=False)
+def get_user(request: Request[User, Dict[Literal["user_id"], str], Any]) -> Any:
+    return request.user
+
+
+session_auth = SessionAuth[User, ServerSideSessionBackend](
+    retrieve_user_handler=retrieve_user_handler,
+    session_backend_config=ServerSideSessionConfig(),
+    exclude=["/login", "/schema"],
+)
+
+
+def test_options_request_with_session_auth() -> None:
+    with create_test_client(
+        route_handlers=[login, get_user],
+        on_app_init=[session_auth.on_app_init],
+    ) as client:
+        response = client.options(get_user.paths.pop())
+        assert response.status_code == HTTP_204_NO_CONTENT