From 70a48e59a2dded3eecb20a658db54f7f114647fa Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Sat, 26 Aug 2023 14:58:47 +0800 Subject: [PATCH 1/6] Add WebSocket handling support for HTTP security dependencies --- fastapi/security/api_key.py | 12 ++++++---- fastapi/security/http.py | 18 ++++++++------- fastapi/security/oauth2.py | 13 ++++++----- fastapi/security/open_id_connect_url.py | 6 +++-- fastapi/security/utils.py | 29 ++++++++++++++++++++++++- 5 files changed, 58 insertions(+), 20 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 496c815a7..416049970 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -3,8 +3,9 @@ from typing import Optional from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase +from fastapi.security.utils import handle_exc_for_ws from starlette.exceptions import HTTPException -from starlette.requests import Request +from starlette.requests import HTTPConnection from starlette.status import HTTP_403_FORBIDDEN from typing_extensions import Annotated @@ -108,7 +109,8 @@ class APIKeyQuery(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.query_params.get(self.model.name) return self.check_api_key(api_key, self.auto_error) @@ -196,7 +198,8 @@ class APIKeyHeader(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.headers.get(self.model.name) return self.check_api_key(api_key, self.auto_error) @@ -284,6 +287,7 @@ class APIKeyCookie(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.cookies.get(self.model.name) return self.check_api_key(api_key, self.auto_error) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 3a5985650..df2cb6b43 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -7,9 +7,9 @@ from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel from fastapi.openapi.models import HTTPBearer as HTTPBearerModel from fastapi.security.base import SecurityBase -from fastapi.security.utils import get_authorization_scheme_param +from fastapi.security.utils import get_authorization_scheme_param, handle_exc_for_ws from pydantic import BaseModel -from starlette.requests import Request +from starlette.requests import HTTPConnection from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN from typing_extensions import Annotated @@ -80,8 +80,9 @@ class HTTPBase(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + @handle_exc_for_ws async def __call__( - self, request: Request + self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) @@ -185,9 +186,8 @@ class HTTPBasic(HTTPBase): self.realm = realm self.auto_error = auto_error - async def __call__( # type: ignore - self, request: Request - ) -> Optional[HTTPBasicCredentials]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[HTTPBasicCredentials]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if self.realm: @@ -299,8 +299,9 @@ class HTTPBearer(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + @handle_exc_for_ws async def __call__( - self, request: Request + self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) @@ -401,8 +402,9 @@ class HTTPDigest(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + @handle_exc_for_ws async def __call__( - self, request: Request + self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: authorization = request.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index f8d97d762..8c15e905f 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -6,8 +6,8 @@ from fastapi.openapi.models import OAuth2 as OAuth2Model from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel from fastapi.param_functions import Form from fastapi.security.base import SecurityBase -from fastapi.security.utils import get_authorization_scheme_param -from starlette.requests import Request +from fastapi.security.utils import get_authorization_scheme_param, handle_exc_for_ws +from starlette.requests import HTTPConnection from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN # TODO: import from typing when deprecating Python 3.9 @@ -377,7 +377,8 @@ class OAuth2(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: @@ -486,7 +487,8 @@ class OAuth2PasswordBearer(OAuth2): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": @@ -596,7 +598,8 @@ class OAuth2AuthorizationCodeBearer(OAuth2): auto_error=auto_error, ) - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if not authorization or scheme.lower() != "bearer": diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 5e99798e6..291160193 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -3,8 +3,9 @@ from typing import Optional from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase +from fastapi.security.utils import handle_exc_for_ws from starlette.exceptions import HTTPException -from starlette.requests import Request +from starlette.requests import HTTPConnection from starlette.status import HTTP_403_FORBIDDEN from typing_extensions import Annotated @@ -73,7 +74,8 @@ class OpenIdConnect(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - async def __call__(self, request: Request) -> Optional[str]: + @handle_exc_for_ws + async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index fa7a450b7..2a0849303 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -1,4 +1,10 @@ -from typing import Optional, Tuple +from functools import wraps +from typing import Any, Awaitable, Callable, Optional, Tuple, TypeVar + +from fastapi.exceptions import HTTPException, WebSocketException +from starlette.requests import HTTPConnection +from starlette.status import WS_1008_POLICY_VIOLATION +from starlette.websockets import WebSocket def get_authorization_scheme_param( @@ -8,3 +14,24 @@ def get_authorization_scheme_param( return "", "" scheme, _, param = authorization_header_value.partition(" ") return scheme, param + + +_SecurityDepFunc = TypeVar( + "_SecurityDepFunc", bound=Callable[[Any, HTTPConnection], Awaitable] +) + + +def handle_exc_for_ws(func: _SecurityDepFunc) -> _SecurityDepFunc: + @wraps(func) + async def wrapper(self, request: HTTPConnection, *args, **kwargs): + try: + return await func(self, request, *args, **kwargs) + except HTTPException as e: + if not isinstance(request, WebSocket): + raise e + await request.accept() + raise WebSocketException( + code=WS_1008_POLICY_VIOLATION, reason=e.detail + ) from None + + return wrapper # type: ignore From 7d071df243655702401c6b77b56dbc256d494a76 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Sat, 26 Aug 2023 15:17:30 +0800 Subject: [PATCH 2/6] Fix linting --- fastapi/security/http.py | 4 +++- fastapi/security/utils.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index df2cb6b43..b8b7dd369 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -187,7 +187,9 @@ class HTTPBasic(HTTPBase): self.auto_error = auto_error @handle_exc_for_ws - async def __call__(self, request: HTTPConnection) -> Optional[HTTPBasicCredentials]: + async def __call__( # type: ignore + self, request: HTTPConnection + ) -> Optional[HTTPBasicCredentials]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if self.realm: diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index 2a0849303..c40d526a7 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -17,15 +17,15 @@ def get_authorization_scheme_param( _SecurityDepFunc = TypeVar( - "_SecurityDepFunc", bound=Callable[[Any, HTTPConnection], Awaitable] + "_SecurityDepFunc", bound=Callable[[Any, HTTPConnection], Awaitable[Any]] ) def handle_exc_for_ws(func: _SecurityDepFunc) -> _SecurityDepFunc: @wraps(func) - async def wrapper(self, request: HTTPConnection, *args, **kwargs): + async def wrapper(self: Any, request: HTTPConnection) -> Any: try: - return await func(self, request, *args, **kwargs) + return await func(self, request) except HTTPException as e: if not isinstance(request, WebSocket): raise e From f40bbafd8da6dd6a55e4196adf2e7cdc30122b21 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Sat, 26 Aug 2023 16:20:34 +0800 Subject: [PATCH 3/6] Add tests for websocket with authorization --- fastapi/security/utils.py | 3 ++- tests/test_security_http_base.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index c40d526a7..3ddf56f1f 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -29,7 +29,8 @@ def handle_exc_for_ws(func: _SecurityDepFunc) -> _SecurityDepFunc: except HTTPException as e: if not isinstance(request, WebSocket): raise e - await request.accept() + # close before accepted with result a HTTP 403 so the exception argument is ignored + # ref: https://asgi.readthedocs.io/en/latest/specs/www.html#close-send-event raise WebSocketException( code=WS_1008_POLICY_VIOLATION, reason=e.detail ) from None diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 51928bafd..5c097939b 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -1,6 +1,8 @@ -from fastapi import FastAPI, Security +import pytest +from fastapi import FastAPI, Security, WebSocket from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient +from starlette.websockets import WebSocketDisconnect app = FastAPI() @@ -12,6 +14,16 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur return {"scheme": credentials.scheme, "credentials": credentials.credentials} +@app.websocket("/users/timeline") +async def read_user_timeline( + websocket: WebSocket, credentials: HTTPAuthorizationCredentials = Security(security) +): + await websocket.accept() + await websocket.send_json( + {"scheme": credentials.scheme, "credentials": credentials.credentials} + ) + + client = TestClient(app) @@ -27,6 +39,21 @@ def test_security_http_base_no_credentials(): assert response.json() == {"detail": "Not authenticated"} +def test_security_http_base_with_ws(): + with client.websocket_connect( + "/users/timeline", headers={"Authorization": "Other foobar"} + ) as websocket: + data = websocket.receive_json() + assert data == {"scheme": "Other", "credentials": "foobar"} + + +def test_security_http_base_with_ws_no_credentials(): + with pytest.raises(WebSocketDisconnect) as e: + with client.websocket_connect("/users/timeline"): + pass + assert e.value.reason == "Not authenticated" + + def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text From cb368d439b0219f993282af14bd1a05e6cf8b8b4 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Tue, 2 Apr 2024 23:01:26 +0800 Subject: [PATCH 4/6] Remove handle_exc_for_ws function from security utils --- fastapi/security/api_key.py | 4 ---- fastapi/security/http.py | 6 +---- fastapi/security/oauth2.py | 5 +---- fastapi/security/open_id_connect_url.py | 2 -- fastapi/security/utils.py | 30 +------------------------ 5 files changed, 3 insertions(+), 44 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 416049970..0eccb99c1 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -3,7 +3,6 @@ from typing import Optional from annotated_doc import Doc from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase -from fastapi.security.utils import handle_exc_for_ws from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection from starlette.status import HTTP_403_FORBIDDEN @@ -109,7 +108,6 @@ class APIKeyQuery(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.query_params.get(self.model.name) return self.check_api_key(api_key, self.auto_error) @@ -198,7 +196,6 @@ class APIKeyHeader(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.headers.get(self.model.name) return self.check_api_key(api_key, self.auto_error) @@ -287,7 +284,6 @@ class APIKeyCookie(APIKeyBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: api_key = request.cookies.get(self.model.name) return self.check_api_key(api_key, self.auto_error) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index b8b7dd369..068214634 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -7,7 +7,7 @@ from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel from fastapi.openapi.models import HTTPBearer as HTTPBearerModel from fastapi.security.base import SecurityBase -from fastapi.security.utils import get_authorization_scheme_param, handle_exc_for_ws +from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import HTTPConnection from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN @@ -80,7 +80,6 @@ class HTTPBase(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__( self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: @@ -186,7 +185,6 @@ class HTTPBasic(HTTPBase): self.realm = realm self.auto_error = auto_error - @handle_exc_for_ws async def __call__( # type: ignore self, request: HTTPConnection ) -> Optional[HTTPBasicCredentials]: @@ -301,7 +299,6 @@ class HTTPBearer(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__( self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: @@ -404,7 +401,6 @@ class HTTPDigest(HTTPBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__( self, request: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 8c15e905f..bb63c415a 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -6,7 +6,7 @@ from fastapi.openapi.models import OAuth2 as OAuth2Model from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel from fastapi.param_functions import Form from fastapi.security.base import SecurityBase -from fastapi.security.utils import get_authorization_scheme_param, handle_exc_for_ws +from fastapi.security.utils import get_authorization_scheme_param from starlette.requests import HTTPConnection from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN @@ -377,7 +377,6 @@ class OAuth2(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: @@ -487,7 +486,6 @@ class OAuth2PasswordBearer(OAuth2): auto_error=auto_error, ) - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) @@ -598,7 +596,6 @@ class OAuth2AuthorizationCodeBearer(OAuth2): auto_error=auto_error, ) - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index 291160193..e7d116885 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -3,7 +3,6 @@ from typing import Optional from annotated_doc import Doc from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase -from fastapi.security.utils import handle_exc_for_ws from starlette.exceptions import HTTPException from starlette.requests import HTTPConnection from starlette.status import HTTP_403_FORBIDDEN @@ -74,7 +73,6 @@ class OpenIdConnect(SecurityBase): self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error - @handle_exc_for_ws async def __call__(self, request: HTTPConnection) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: diff --git a/fastapi/security/utils.py b/fastapi/security/utils.py index 3ddf56f1f..fa7a450b7 100644 --- a/fastapi/security/utils.py +++ b/fastapi/security/utils.py @@ -1,10 +1,4 @@ -from functools import wraps -from typing import Any, Awaitable, Callable, Optional, Tuple, TypeVar - -from fastapi.exceptions import HTTPException, WebSocketException -from starlette.requests import HTTPConnection -from starlette.status import WS_1008_POLICY_VIOLATION -from starlette.websockets import WebSocket +from typing import Optional, Tuple def get_authorization_scheme_param( @@ -14,25 +8,3 @@ def get_authorization_scheme_param( return "", "" scheme, _, param = authorization_header_value.partition(" ") return scheme, param - - -_SecurityDepFunc = TypeVar( - "_SecurityDepFunc", bound=Callable[[Any, HTTPConnection], Awaitable[Any]] -) - - -def handle_exc_for_ws(func: _SecurityDepFunc) -> _SecurityDepFunc: - @wraps(func) - async def wrapper(self: Any, request: HTTPConnection) -> Any: - try: - return await func(self, request) - except HTTPException as e: - if not isinstance(request, WebSocket): - raise e - # close before accepted with result a HTTP 403 so the exception argument is ignored - # ref: https://asgi.readthedocs.io/en/latest/specs/www.html#close-send-event - raise WebSocketException( - code=WS_1008_POLICY_VIOLATION, reason=e.detail - ) from None - - return wrapper # type: ignore From 293c1810318bd9e00745b75dabd499979955ea14 Mon Sep 17 00:00:00 2001 From: Mix <32300164+mnixry@users.noreply.github.com> Date: Wed, 3 Apr 2024 13:58:24 +0800 Subject: [PATCH 5/6] Refactor test script with `auto_error=False` case. --- tests/test_security_http_base.py | 29 +---------------------- tests/test_security_http_base_optional.py | 29 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 5c097939b..51928bafd 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -1,8 +1,6 @@ -import pytest -from fastapi import FastAPI, Security, WebSocket +from fastapi import FastAPI, Security from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient -from starlette.websockets import WebSocketDisconnect app = FastAPI() @@ -14,16 +12,6 @@ def read_current_user(credentials: HTTPAuthorizationCredentials = Security(secur return {"scheme": credentials.scheme, "credentials": credentials.credentials} -@app.websocket("/users/timeline") -async def read_user_timeline( - websocket: WebSocket, credentials: HTTPAuthorizationCredentials = Security(security) -): - await websocket.accept() - await websocket.send_json( - {"scheme": credentials.scheme, "credentials": credentials.credentials} - ) - - client = TestClient(app) @@ -39,21 +27,6 @@ def test_security_http_base_no_credentials(): assert response.json() == {"detail": "Not authenticated"} -def test_security_http_base_with_ws(): - with client.websocket_connect( - "/users/timeline", headers={"Authorization": "Other foobar"} - ) as websocket: - data = websocket.receive_json() - assert data == {"scheme": "Other", "credentials": "foobar"} - - -def test_security_http_base_with_ws_no_credentials(): - with pytest.raises(WebSocketDisconnect) as e: - with client.websocket_connect("/users/timeline"): - pass - assert e.value.reason == "Not authenticated" - - def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text diff --git a/tests/test_security_http_base_optional.py b/tests/test_security_http_base_optional.py index dd4d76843..4da9e82bc 100644 --- a/tests/test_security_http_base_optional.py +++ b/tests/test_security_http_base_optional.py @@ -1,6 +1,6 @@ from typing import Optional -from fastapi import FastAPI, Security +from fastapi import FastAPI, Security, WebSocket from fastapi.security.http import HTTPAuthorizationCredentials, HTTPBase from fastapi.testclient import TestClient @@ -18,6 +18,19 @@ def read_current_user( return {"scheme": credentials.scheme, "credentials": credentials.credentials} +@app.websocket("/users/timeline") +async def read_user_timeline( + websocket: WebSocket, + credentials: Optional[HTTPAuthorizationCredentials] = Security(security), +): + await websocket.accept() + await websocket.send_json( + {"scheme": credentials.scheme, "credentials": credentials.credentials} + if credentials + else {"msg": "Create an account first"} + ) + + client = TestClient(app) @@ -33,6 +46,20 @@ def test_security_http_base_no_credentials(): assert response.json() == {"msg": "Create an account first"} +def test_security_http_base_with_ws(): + with client.websocket_connect( + "/users/timeline", headers={"Authorization": "Other foobar"} + ) as websocket: + data = websocket.receive_json() + assert data == {"scheme": "Other", "credentials": "foobar"} + + +def test_security_http_base_with_ws_no_credentials(): + with client.websocket_connect("/users/timeline") as websocket: + data = websocket.receive_json() + assert data == {"msg": "Create an account first"} + + def test_openapi_schema(): response = client.get("/openapi.json") assert response.status_code == 200, response.text From aa725b3030f26e398b73e9e8a51916e9253d3fc4 Mon Sep 17 00:00:00 2001 From: HexMix <32300164+mnixry@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:32:02 +0800 Subject: [PATCH 6/6] Refactor HTTP security classes to use `conn` as variable name --- fastapi/security/http.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 068214634..2012d0558 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -81,9 +81,9 @@ class HTTPBase(SecurityBase): self.auto_error = auto_error async def __call__( - self, request: HTTPConnection + self, conn: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: - authorization = request.headers.get("Authorization") + authorization = conn.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: @@ -186,9 +186,9 @@ class HTTPBasic(HTTPBase): self.auto_error = auto_error async def __call__( # type: ignore - self, request: HTTPConnection + self, conn: HTTPConnection ) -> Optional[HTTPBasicCredentials]: - authorization = request.headers.get("Authorization") + authorization = conn.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if self.realm: unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} @@ -300,9 +300,9 @@ class HTTPBearer(HTTPBase): self.auto_error = auto_error async def __call__( - self, request: HTTPConnection + self, conn: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: - authorization = request.headers.get("Authorization") + authorization = conn.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: @@ -402,9 +402,9 @@ class HTTPDigest(HTTPBase): self.auto_error = auto_error async def __call__( - self, request: HTTPConnection + self, conn: HTTPConnection ) -> Optional[HTTPAuthorizationCredentials]: - authorization = request.headers.get("Authorization") + authorization = conn.headers.get("Authorization") scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: