Repository: litestar-org/litestar

Issue:
Abstract Authentication Middleware should respect `error_handlers` defined at the app/router level?
Should the `AbstractAuthenticationMiddleware` use the `exception_handlers`?

Given the example below, the `exeption_formatter` is not called for `NotAuthorized` exception, and you have to override the `error_response` method to return the custom exception format.

```python
from typing import Union

from pydantic import BaseModel
from starlette.requests import HTTPConnection

from starlite import (
    AbstractAuthenticationMiddleware,
    AuthenticationResult,
    Controller,
    HTTPException,
    MediaType,
    NotAuthorizedException,
    Request,
    Response,
    Router,
    Starlite,
    get,
)
from starlite.exceptions.exceptions import PermissionDeniedException


class ExceptionResponse(BaseModel):
    status_code: int
    message: str


def exception_formatter(_: Request, exception: Exception) -> Response:
    return Response(
        content=ExceptionResponse(
            status_code=getattr(exception, "status_code", 500), message=getattr(exception, "detail", "")
        ),
        status_code=getattr(exception, "status_code", 500),
        media_type=MediaType.JSON,
    )


class AuthenticationMiddleware(AbstractAuthenticationMiddleware):
    async def authenticate_request(self, request: HTTPConnection) -> AuthenticationResult:

        if not (token := request.headers.get("Authorization")):
            raise NotAuthorizedException()

        return AuthenticationResult(user=None, auth=token)


class MyController(Controller):
    path = "/needs-auth"

    @get()
    async def needs_auth(self) -> str:
        return "needs auth token"


router = Router(
    path="/api",
    route_handlers=[MyController],
    middleware=[AuthenticationMiddleware],
)

app = Starlite(
    route_handlers=[router],
    exception_handlers={HTTPException: exception_formatter},
)
```


Discussion:

Resolving patch:
diff --git a/starlite/app.py b/starlite/app.py
--- a/starlite/app.py
+++ b/starlite/app.py
@@ -77,7 +77,7 @@ class HandlerIndex(TypedDict):
 
     path: str
     """Full route path to the route handler."""
-    handler: Union[Union["HTTPRouteHandler", "WebsocketRouteHandler", "ASGIRouteHandler"]]
+    handler: Union["HTTPRouteHandler", "WebsocketRouteHandler", "ASGIRouteHandler"]
     """Route handler instance."""
 
 
diff --git a/starlite/middleware/authentication.py b/starlite/middleware/authentication.py
--- a/starlite/middleware/authentication.py
+++ b/starlite/middleware/authentication.py
@@ -1,13 +1,12 @@
+import re
 from abc import ABC, abstractmethod
-from typing import TYPE_CHECKING, Any, Union
+from typing import TYPE_CHECKING, Any, List, Optional, Pattern, Union
 
 from pydantic import BaseConfig, BaseModel
 from starlette.requests import HTTPConnection
 
-from starlite.enums import MediaType, ScopeType
-from starlite.exceptions import NotAuthorizedException, PermissionDeniedException
+from starlite.enums import ScopeType
 from starlite.middleware.base import MiddlewareProtocol
-from starlite.response import Response
 
 if TYPE_CHECKING:
     from starlette.types import ASGIApp, Receive, Scope, Send
@@ -34,56 +33,35 @@ class AbstractAuthenticationMiddleware(ABC, MiddlewareProtocol):
     """
     Scopes supported by the middleware.
     """
-    error_response_media_type = MediaType.JSON
-    """
-    The 'Content-Type' to use for error responses.
-    """
-    websocket_error_status_code = 4000
-    """
-    The status code to for websocket authentication errors.
-    """
 
-    def __init__(self, app: "ASGIApp"):
+    def __init__(
+        self,
+        app: "ASGIApp",
+        exclude: Optional[Union[str, List[str]]] = None,
+    ):
         """This is an abstract AuthenticationMiddleware that allows users to
         create their own AuthenticationMiddleware by extending it and
         overriding the 'authenticate_request' method.
 
         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.
         """
         super().__init__(app)
         self.app = app
+        self.exclude: Optional[Pattern[str]] = None
+        if exclude:
+            self.exclude = re.compile("|".join(exclude)) if isinstance(exclude, list) else re.compile(exclude)
 
     async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
-        try:
+        if self.exclude and self.exclude.findall(scope["path"]):
+            await self.app(scope, receive, send)
+        else:
             if scope["type"] in self.scopes:
                 auth_result = await self.authenticate_request(HTTPConnection(scope))
                 scope["user"] = auth_result.user
                 scope["auth"] = auth_result.auth
             await self.app(scope, receive, send)
-        except (NotAuthorizedException, PermissionDeniedException) as e:
-            if scope["type"] == ScopeType.WEBSOCKET:  # pragma: no cover
-                await send({"type": "websocket.close", "code": self.websocket_error_status_code, "reason": repr(e)})
-            else:
-                response = self.create_error_response(exc=e)
-                await response(scope, receive, send)
-
-    def create_error_response(self, exc: Union[NotAuthorizedException, PermissionDeniedException]) -> Response:
-        """Creates an Error response from the given exceptions, defaults to a
-        JSON response.
-
-        Args:
-            exc: Either an [NotAuthorizedException][starlite.exceptions.NotAuthorizedException] or
-                [PermissionDeniedException][starlite.exceptions.PermissionDeniedException] instance.
-
-        Returns:
-            A [Response][starlite.response.Response] instance.
-        """
-        return Response(
-            media_type=self.error_response_media_type,
-            content={"detail": exc.detail, "extra": exc.extra},
-            status_code=exc.status_code,
-        )
 
     @abstractmethod
     async def authenticate_request(self, request: HTTPConnection) -> AuthenticationResult:  # pragma: no cover
diff --git a/starlite/types.py b/starlite/types.py
--- a/starlite/types.py
+++ b/starlite/types.py
@@ -56,7 +56,11 @@
 H = TypeVar("H", bound=HTTPConnection)
 
 Middleware = Union[
-    StarletteMiddleware, DefineMiddleware, Type[BaseHTTPMiddleware], Type[MiddlewareProtocol], Callable[..., ASGIApp]
+    StarletteMiddleware,
+    DefineMiddleware,
+    Type[BaseHTTPMiddleware],
+    Type[MiddlewareProtocol],
+    Callable[..., ASGIApp],
 ]
 ResponseType = Type[Response]
 ExceptionHandler = Callable[


Test patch:
diff --git a/tests/middleware/test_authentication.py b/tests/middleware/test_authentication.py
--- a/tests/middleware/test_authentication.py
+++ b/tests/middleware/test_authentication.py
@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, Dict
+from typing import TYPE_CHECKING, Any, Dict
 
 import pytest
 from pydantic import BaseModel
@@ -16,12 +16,17 @@
     AbstractAuthenticationMiddleware,
     AuthenticationResult,
 )
+from starlite.middleware.base import DefineMiddleware
 from starlite.testing import create_test_client
 
 if TYPE_CHECKING:
     from starlette.requests import HTTPConnection
 
 
+async def dummy_app(scope: Any, receive: Any, send: Any) -> None:
+    return None
+
+
 class User(BaseModel):
     name: str
     id: int
@@ -124,3 +129,32 @@ async def route_handler(socket: WebSocket[User, Auth]) -> None:
     client = create_test_client(route_handlers=route_handler)
     with pytest.raises(WebSocketDisconnect), client.websocket_connect("/", headers={"Authorization": "yep"}) as ws:
         ws.receive_json()
+
+
+def test_authentication_middleware_exclude() -> None:
+    auth_mw = DefineMiddleware(AuthMiddleware, exclude=["north", "south"])
+
+    @get("/north/{value:int}")
+    def north_handler(value: int) -> Dict[str, int]:
+        return {"value": value}
+
+    @get("/south")
+    def south_handler() -> None:
+        return None
+
+    @get("/west")
+    def west_handler() -> None:
+        return None
+
+    with create_test_client(
+        route_handlers=[north_handler, south_handler, west_handler],
+        middleware=[auth_mw],
+    ) as client:
+        response = client.get("/north/1")
+        assert response.status_code == HTTP_200_OK
+
+        response = client.get("/south")
+        assert response.status_code == HTTP_200_OK
+
+        response = client.get("/west")
+        assert response.status_code == HTTP_403_FORBIDDEN