From 19daeca2e22c9c3c2d299a74dbc2e0428c9b0f4c Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 9 Jun 2025 12:29:37 +0200 Subject: [PATCH 01/12] Move common logic of `APIKey**` classes to `APIKeyBase` class --- fastapi/security/api_key.py | 55 ++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index 70c2dca8a..d0616c73f 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, Union from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase @@ -9,10 +9,27 @@ from typing_extensions import Annotated, Doc class APIKeyBase(SecurityBase): - @staticmethod - def check_api_key(api_key: Optional[str], auto_error: bool) -> Optional[str]: + def __init__( + self, + location: APIKeyIn, + name: str, + description: Union[str, None], + scheme_name: Union[str, None], + auto_error: bool, + ): + self.parameter_location = location.value + self.parameter_name = name + self.auto_error = auto_error + self.model: APIKey = APIKey( + **{"in": location}, # type: ignore[arg-type] + name=name, + description=description, + ) + self.scheme_name = scheme_name or self.__class__.__name__ + + def check_api_key(self, api_key: Optional[str]) -> Optional[str]: if not api_key: - if auto_error: + if self.auto_error: raise HTTPException( status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" ) @@ -99,17 +116,17 @@ class APIKeyQuery(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.query}, # type: ignore[arg-type] + super().__init__( + location=APIKeyIn.query, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.query_params.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyHeader(APIKeyBase): @@ -187,17 +204,17 @@ class APIKeyHeader(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.header}, # type: ignore[arg-type] + super().__init__( + location=APIKeyIn.header, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.headers.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) class APIKeyCookie(APIKeyBase): @@ -275,14 +292,14 @@ class APIKeyCookie(APIKeyBase): ), ] = True, ): - self.model: APIKey = APIKey( - **{"in": APIKeyIn.cookie}, # type: ignore[arg-type] + super().__init__( + location=APIKeyIn.cookie, name=name, + scheme_name=scheme_name, description=description, + auto_error=auto_error, ) - self.scheme_name = scheme_name or self.__class__.__name__ - self.auto_error = auto_error async def __call__(self, request: Request) -> Optional[str]: api_key = request.cookies.get(self.model.name) - return self.check_api_key(api_key, self.auto_error) + return self.check_api_key(api_key) From c653eeff1eb978acede84e9ccf4fd8b33813f69c Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Mon, 9 Jun 2025 12:45:33 +0200 Subject: [PATCH 02/12] Fix `APIKey**` security schemes status code on "Not authenticated" error --- fastapi/security/api_key.py | 121 +++++++++++++++++- tests/test_security_api_key_cookie.py | 3 +- ...est_security_api_key_cookie_description.py | 3 +- tests/test_security_api_key_header.py | 3 +- ...est_security_api_key_header_description.py | 3 +- tests/test_security_api_key_query.py | 3 +- ...test_security_api_key_query_description.py | 3 +- tests/test_security_status_code_403_option.py | 56 ++++++++ 8 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 tests/test_security_status_code_403_option.py diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index d0616c73f..b1388f92c 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,11 +1,11 @@ -from typing import Optional, Union +from typing import Literal, Optional, Union from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc, deprecated class APIKeyBase(SecurityBase): @@ -16,10 +16,13 @@ class APIKeyBase(SecurityBase): description: Union[str, None], scheme_name: Union[str, None], auto_error: bool, + not_authenticated_status_code: Literal[401, 403], ): self.parameter_location = location.value self.parameter_name = name self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code + self.model: APIKey = APIKey( **{"in": location}, # type: ignore[arg-type] name=name, @@ -27,12 +30,31 @@ class APIKeyBase(SecurityBase): ) self.scheme_name = scheme_name or self.__class__.__name__ + def format_www_authenticate_header_value(self) -> str: + """ + The WWW-Authenticate header is not standardized for API Key authentication. + It's considered good practice to include information about the authentication + challange. + This method follows one of the common templates. + If a different format is required, override this method in a subclass. + """ + + return f'ApiKey in="{self.parameter_location}", name="{self.parameter_name}"' + def check_api_key(self, api_key: Optional[str]) -> Optional[str]: if not api_key: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + else: # By default use 401 + www_authenticate = self.format_www_authenticate_header_value() + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": www_authenticate}, + ) return None return api_key @@ -115,6 +137,34 @@ class APIKeyQuery(APIKeyBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if the query parameter is not provided and `auto_error` is + set to `True`, `APIKeyQuery` will automatically raise an + `HTTPException` with the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + Consider updating your clients to align with the new behavior. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): super().__init__( location=APIKeyIn.query, @@ -122,6 +172,7 @@ class APIKeyQuery(APIKeyBase): scheme_name=scheme_name, description=description, auto_error=auto_error, + not_authenticated_status_code=not_authenticated_status_code, ) async def __call__(self, request: Request) -> Optional[str]: @@ -203,6 +254,34 @@ class APIKeyHeader(APIKeyBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if the header is not provided and `auto_error` is + set to `True`, `APIKeyHeader` will automatically raise an + `HTTPException` with the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + Consider updating your clients to align with the new behavior. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): super().__init__( location=APIKeyIn.header, @@ -210,6 +289,7 @@ class APIKeyHeader(APIKeyBase): scheme_name=scheme_name, description=description, auto_error=auto_error, + not_authenticated_status_code=not_authenticated_status_code, ) async def __call__(self, request: Request) -> Optional[str]: @@ -291,6 +371,34 @@ class APIKeyCookie(APIKeyBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if the cookie is not provided and `auto_error` is + set to `True`, `APIKeyCookie` will automatically raise an + `HTTPException` with the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + Consider updating your clients to align with the new behavior. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): super().__init__( location=APIKeyIn.cookie, @@ -298,6 +406,7 @@ class APIKeyCookie(APIKeyBase): scheme_name=scheme_name, description=description, auto_error=auto_error, + not_authenticated_status_code=not_authenticated_status_code, ) async def __call__(self, request: Request) -> Optional[str]: diff --git a/tests/test_security_api_key_cookie.py b/tests/test_security_api_key_cookie.py index 4ddb8e2ee..488503817 100644 --- a/tests/test_security_api_key_cookie.py +++ b/tests/test_security_api_key_cookie.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="cookie", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_api_key_cookie_description.py b/tests/test_security_api_key_cookie_description.py index d99d616e0..e0e448471 100644 --- a/tests/test_security_api_key_cookie_description.py +++ b/tests/test_security_api_key_cookie_description.py @@ -32,8 +32,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): client = TestClient(app) response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="cookie", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_api_key_header.py b/tests/test_security_api_key_header.py index 1ff883703..b72d258c4 100644 --- a/tests/test_security_api_key_header.py +++ b/tests/test_security_api_key_header.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="header", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_api_key_header_description.py b/tests/test_security_api_key_header_description.py index 27f9d0f29..70b85af03 100644 --- a/tests/test_security_api_key_header_description.py +++ b/tests/test_security_api_key_header_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="header", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_api_key_query.py b/tests/test_security_api_key_query.py index dc7a0a621..7a01101c4 100644 --- a/tests/test_security_api_key_query.py +++ b/tests/test_security_api_key_query.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="query", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_api_key_query_description.py b/tests/test_security_api_key_query_description.py index 35dc7743a..45102eb17 100644 --- a/tests/test_security_api_key_query_description.py +++ b/tests/test_security_api_key_query_description.py @@ -33,8 +33,9 @@ def test_security_api_key(): def test_security_api_key_no_key(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == 'ApiKey in="query", name="key"' def test_openapi_schema(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py new file mode 100644 index 000000000..ccc4d216a --- /dev/null +++ b/tests/test_security_status_code_403_option.py @@ -0,0 +1,56 @@ +import pytest +from fastapi import FastAPI, Security +from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyHeader, APIKeyQuery +from fastapi.testclient import TestClient + + +@pytest.mark.parametrize( + "auth", + [ + APIKeyQuery(name="key", not_authenticated_status_code=403), + APIKeyHeader(name="key", not_authenticated_status_code=403), + APIKeyCookie(name="key", not_authenticated_status_code=403), + ], +) +def test_apikey_status_code_403_on_auth_error(auth: APIKeyBase): + """ + Test temporary `not_authenticated_status_code` parameter for APIKey** classes. + """ + + app = FastAPI() + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +@pytest.mark.parametrize( + "auth", + [ + APIKeyQuery(name="key", not_authenticated_status_code=403, auto_error=False), + APIKeyHeader(name="key", not_authenticated_status_code=403, auto_error=False), + APIKeyCookie(name="key", not_authenticated_status_code=403, auto_error=False), + ], +) +def test_apikey_status_code_403_on_auth_error_no_auto_error(auth: APIKeyBase): + """ + Test temporary `not_authenticated_status_code` parameter for APIKey** classes with + `auto_error=False`. + """ + + app = FastAPI() + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 200 From 6c53a67422431d076e50e5d52946dec8062a7564 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 12:53:27 +0200 Subject: [PATCH 03/12] Fix `HTTPBearer` security scheme status code on "Not authenticated" error --- fastapi/security/http.py | 51 +++++++++++++++--- tests/test_security_http_bearer.py | 6 ++- .../test_security_http_bearer_description.py | 6 ++- tests/test_security_status_code_403_option.py | 54 +++++++++++++++++++ 4 files changed, 105 insertions(+), 12 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 9ab2df3c9..1e0f35bff 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Optional +from typing import Literal, Optional from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel @@ -10,7 +10,7 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated, Doc, deprecated class HTTPBasicCredentials(BaseModel): @@ -293,10 +293,38 @@ class HTTPBearer(HTTPBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if the HTTP Bearer token is not provided and `auto_error` + is set to `True`, `HTTPBearer` will automatically raise an + `HTTPException` with the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): self.model = HTTPBearerModel(bearerFormat=bearerFormat, description=description) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code async def __call__( self, request: Request @@ -305,21 +333,28 @@ class HTTPBearer(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + self._raise_not_authenticated_error(error_message="Not authenticated") else: return None if scheme.lower() != "bearer": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", + self._raise_not_authenticated_error( + error_message="Invalid authentication credentials" ) else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) + def _raise_not_authenticated_error(self, error_message: str): + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=error_message) + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=error_message, + headers={"WWW-Authenticate": "Bearer"}, + ) + class HTTPDigest(HTTPBase): """ diff --git a/tests/test_security_http_bearer.py b/tests/test_security_http_bearer.py index 5b9e2d691..de4e0427a 100644 --- a/tests/test_security_http_bearer.py +++ b/tests/test_security_http_bearer.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_http_bearer_description.py b/tests/test_security_http_bearer_description.py index 2f11c3a14..f87df5434 100644 --- a/tests/test_security_http_bearer_description.py +++ b/tests/test_security_http_bearer_description.py @@ -23,14 +23,16 @@ def test_security_http_bearer(): def test_security_http_bearer_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_security_http_bearer_incorrect_scheme_credentials(): response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index ccc4d216a..db5e15ed4 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -1,6 +1,8 @@ import pytest from fastapi import FastAPI, Security +from fastapi.openapi.models import HTTPBase from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyHeader, APIKeyQuery +from fastapi.security.http import HTTPBearer, HTTPDigest from fastapi.testclient import TestClient @@ -54,3 +56,55 @@ def test_apikey_status_code_403_on_auth_error_no_auto_error(auth: APIKeyBase): response = client.get("/") assert response.status_code == 200 + + +@pytest.mark.parametrize( + "auth", + [ + HTTPBearer(not_authenticated_status_code=403), + ], +) +def test_oauth2_status_code_403_on_auth_error(auth: HTTPBase): + """ + Test temporary `not_authenticated_status_code` parameter for security classes that + follow rfc6750. + """ + + app = FastAPI() + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +@pytest.mark.parametrize( + "auth", + [ + HTTPBearer(not_authenticated_status_code=403, auto_error=False), + ], +) +def test_oauth2_status_code_403_on_auth_error_no_auto_error( + auth: HTTPBase, +): + """ + Test temporary `not_authenticated_status_code` parameter for security classes that + follow rfc6750. + With `auto_error=False`. Response code should be 200 + """ + + app = FastAPI() + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 200 From 51503835f3bf526e31ffa62a65f1782728209516 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 12:54:10 +0200 Subject: [PATCH 04/12] Fix `HTTPDigest` security scheme status code on "Not authenticated" error --- fastapi/security/http.py | 47 ++++++++++++++++--- tests/test_security_http_digest.py | 6 ++- .../test_security_http_digest_description.py | 6 ++- tests/test_security_status_code_403_option.py | 40 ++++++++++++++++ 4 files changed, 89 insertions(+), 10 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 1e0f35bff..16abf9b65 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -430,10 +430,38 @@ class HTTPDigest(HTTPBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if the HTTP Bearer token is not provided and `auto_error` + is set to `True`, `HTTPBearer` will automatically raise an + `HTTPException` with the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): self.model = HTTPBaseModel(scheme="digest", description=description) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code async def __call__( self, request: Request @@ -442,17 +470,24 @@ class HTTPDigest(HTTPBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + self._raise_not_authenticated_error(error_message="Not authenticated") else: return None if scheme.lower() != "digest": if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, - detail="Invalid authentication credentials", + self._raise_not_authenticated_error( + error_message="Invalid authentication credentials", ) else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) + + def _raise_not_authenticated_error(self, error_message: str): + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=error_message) + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail=error_message, + headers={"WWW-Authenticate": "Digest"}, + ) diff --git a/tests/test_security_http_digest.py b/tests/test_security_http_digest.py index 133d35763..a195430d2 100644 --- a/tests/test_security_http_digest.py +++ b/tests/test_security_http_digest.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_http_digest_description.py b/tests/test_security_http_digest_description.py index 4e31a0c00..0ced8494e 100644 --- a/tests/test_security_http_digest_description.py +++ b/tests/test_security_http_digest_description.py @@ -23,16 +23,18 @@ def test_security_http_digest(): def test_security_http_digest_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_security_http_digest_incorrect_scheme_credentials(): response = client.get( "/users/me", headers={"Authorization": "Other invalidauthorization"} ) - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Invalid authentication credentials"} + assert response.headers["WWW-Authenticate"] == "Digest" def test_openapi_schema(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index db5e15ed4..dcd91d2e5 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -108,3 +108,43 @@ def test_oauth2_status_code_403_on_auth_error_no_auto_error( response = client.get("/") assert response.status_code == 200 + + +def test_digest_status_code_403_on_auth_error(): + """ + Test temporary `not_authenticated_status_code` parameter for `Digest` scheme. + """ + + app = FastAPI() + + auth = HTTPDigest(not_authenticated_status_code=403) + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_digest_status_code_403_on_auth_error_no_auto_error(): + """ + Test temporary `not_authenticated_status_code` parameter for `Digest` scheme with + `auto_error=False`. + """ + + app = FastAPI() + + auth = HTTPDigest(not_authenticated_status_code=403, auto_error=False) + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 200 From 0ebeb3fe4f2474dbf3d1917ade102ca9029ac861 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 13:06:29 +0200 Subject: [PATCH 05/12] Fixed import of `HTTPBase` class --- tests/test_security_status_code_403_option.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index dcd91d2e5..084236298 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -1,8 +1,7 @@ import pytest from fastapi import FastAPI, Security -from fastapi.openapi.models import HTTPBase from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyHeader, APIKeyQuery -from fastapi.security.http import HTTPBearer, HTTPDigest +from fastapi.security.http import HTTPBase, HTTPBearer, HTTPDigest from fastapi.testclient import TestClient From 54d60db33d317090fb4669c37f3708cdd0b306f1 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 13:08:01 +0200 Subject: [PATCH 06/12] Fix `HTTPBase` class status code on "Not authenticated" error --- fastapi/security/http.py | 16 ++++++-- tests/test_security_http_base.py | 3 +- tests/test_security_http_base_description.py | 3 +- tests/test_security_status_code_403_option.py | 40 +++++++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 16abf9b65..f332bcf0f 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -74,10 +74,13 @@ class HTTPBase(SecurityBase): scheme_name: Optional[str] = None, description: Optional[str] = None, auto_error: bool = True, + not_authenticated_status_code: Literal[401, 403] = 401, ): self.model = HTTPBaseModel(scheme=scheme, description=description) + self.model_scheme = scheme self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code async def __call__( self, request: Request @@ -86,9 +89,16 @@ class HTTPBase(SecurityBase): scheme, credentials = get_authorization_scheme_param(authorization) if not (authorization and scheme and credentials): if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": self.model_scheme}, + ) else: return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) diff --git a/tests/test_security_http_base.py b/tests/test_security_http_base.py index 51928bafd..8cf259a75 100644 --- a/tests/test_security_http_base.py +++ b/tests/test_security_http_base.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_http_base_description.py b/tests/test_security_http_base_description.py index bc79f3242..791ea59f4 100644 --- a/tests/test_security_http_base_description.py +++ b/tests/test_security_http_base_description.py @@ -23,8 +23,9 @@ def test_security_http_base(): def test_security_http_base_no_credentials(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Other" def test_openapi_schema(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index 084236298..bf91f6202 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -147,3 +147,43 @@ def test_digest_status_code_403_on_auth_error_no_auto_error(): response = client.get("/") assert response.status_code == 200 + + +def test_httpbase_status_code_403_on_auth_error(): + """ + Test temporary `not_authenticated_status_code` parameter for `HTTPBase` class. + """ + + app = FastAPI() + + auth = HTTPBase(scheme="Other", not_authenticated_status_code=403) + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 403 + assert response.json() == {"detail": "Not authenticated"} + + +def test_httpbase_status_code_403_on_auth_error_no_auto_error(): + """ + Test temporary `not_authenticated_status_code` parameter for `HTTPBase` class with + `auto_error=False`. + """ + + app = FastAPI() + + auth = HTTPBase(scheme="Other", not_authenticated_status_code=403, auto_error=False) + + @app.get("/") + async def protected(_: str = Security(auth)): + pass # pragma: no cover + + client = TestClient(app) + + response = client.get("/") + assert response.status_code == 200 From a9fcb9aada5a95c256d74ded2f7ada9b5c25d83d Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 13:24:46 +0200 Subject: [PATCH 07/12] Fix type in the descr of `not_authenticated_status_code` for HTTPDigest --- fastapi/security/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index f332bcf0f..5a550ff35 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -444,8 +444,8 @@ class HTTPDigest(HTTPBase): Literal[401, 403], Doc( """ - By default, if the HTTP Bearer token is not provided and `auto_error` - is set to `True`, `HTTPBearer` will automatically raise an + By default, if the HTTP Digest is not provided and `auto_error` + is set to `True`, `HTTPDigest` will automatically raise an `HTTPException` with the status code `401`. If your client relies on the old (incorrect) behavior and expects the From b9f29b8ee708ffdb0c9f04cc78a37c71aea3880f Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 13:27:15 +0200 Subject: [PATCH 08/12] Fix `OpenIdConnect` security scheme status code on "Not authenticated" error --- fastapi/security/open_id_connect_url.py | 47 ++++++++++++++++--- tests/test_security_openid_connect.py | 3 +- ...est_security_openid_connect_description.py | 3 +- tests/test_security_status_code_403_option.py | 13 ++++- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index c8cceb911..c6b961ac0 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,11 +1,11 @@ -from typing import Optional +from typing import Literal, Optional from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request -from starlette.status import HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc +from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN +from typing_extensions import Annotated, Doc, deprecated class OpenIdConnect(SecurityBase): @@ -65,20 +65,55 @@ class OpenIdConnect(SecurityBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if no HTTP Authorization header provided and `auto_error` + is set to `True`, it will automatically raise an`HTTPException` with + the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): self.model = OpenIdConnectModel( openIdConnectUrl=openIdConnectUrl, description=description ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) else: return None return authorization diff --git a/tests/test_security_openid_connect.py b/tests/test_security_openid_connect.py index 1e322e640..c9a0a8db7 100644 --- a/tests/test_security_openid_connect.py +++ b/tests/test_security_openid_connect.py @@ -39,8 +39,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_openid_connect_description.py b/tests/test_security_openid_connect_description.py index 44cf57f86..d008cbc63 100644 --- a/tests/test_security_openid_connect_description.py +++ b/tests/test_security_openid_connect_description.py @@ -41,8 +41,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_openapi_schema(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index bf91f6202..29866bfd4 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -1,7 +1,10 @@ +from typing import Union + import pytest from fastapi import FastAPI, Security from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyHeader, APIKeyQuery from fastapi.security.http import HTTPBase, HTTPBearer, HTTPDigest +from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient @@ -61,9 +64,10 @@ def test_apikey_status_code_403_on_auth_error_no_auto_error(auth: APIKeyBase): "auth", [ HTTPBearer(not_authenticated_status_code=403), + OpenIdConnect(not_authenticated_status_code=403, openIdConnectUrl="/openid"), ], ) -def test_oauth2_status_code_403_on_auth_error(auth: HTTPBase): +def test_oauth2_status_code_403_on_auth_error(auth: Union[HTTPBase, OpenIdConnect]): """ Test temporary `not_authenticated_status_code` parameter for security classes that follow rfc6750. @@ -86,10 +90,15 @@ def test_oauth2_status_code_403_on_auth_error(auth: HTTPBase): "auth", [ HTTPBearer(not_authenticated_status_code=403, auto_error=False), + OpenIdConnect( + not_authenticated_status_code=403, + openIdConnectUrl="/openid", + auto_error=False, + ), ], ) def test_oauth2_status_code_403_on_auth_error_no_auto_error( - auth: HTTPBase, + auth: Union[HTTPBase, OpenIdConnect], ): """ Test temporary `not_authenticated_status_code` parameter for security classes that From ade9d830f60fb4823d56f9e798cf554299b442d0 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 19:10:08 +0200 Subject: [PATCH 09/12] Fix `OAuth2` security scheme status code on "Not authenticated" error --- fastapi/security/oauth2.py | 44 +++++++++++++++++-- tests/test_security_oauth2.py | 3 +- tests/test_security_status_code_403_option.py | 11 +++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/fastapi/security/oauth2.py b/fastapi/security/oauth2.py index 5ffad5986..5d2b80d2c 100644 --- a/fastapi/security/oauth2.py +++ b/fastapi/security/oauth2.py @@ -10,7 +10,7 @@ from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN # TODO: import from typing when deprecating Python 3.9 -from typing_extensions import Annotated, Doc +from typing_extensions import Annotated, Doc, Literal, deprecated class OAuth2PasswordRequestForm: @@ -369,20 +369,56 @@ class OAuth2(SecurityBase): """ ), ] = True, + not_authenticated_status_code: Annotated[ + Literal[401, 403], + Doc( + """ + By default, if no HTTP Authorization header provided and `auto_error` + is set to `True`, it will automatically raise an`HTTPException` with + the status code `401`. + + If your client relies on the old (incorrect) behavior and expects the + status code to be `403`, you can set `not_authenticated_status_code` to + `403` to achieve it. + + Keep in mind that this parameter is temporary and will be removed in + the near future. + """ + ), + deprecated( + """ + This parameter is temporary. It was introduced to give users time + to upgrade their clients to follow the new behavior and will eventually + be removed. + + Use it as a short-term workaround, but consider updating your clients + to align with the new behavior. + """ + ), + ] = 401, ): self.model = OAuth2Model( flows=cast(OAuthFlowsModel, flows), description=description ) self.scheme_name = scheme_name or self.__class__.__name__ self.auto_error = auto_error + self.not_authenticated_status_code = not_authenticated_status_code async def __call__(self, request: Request) -> Optional[str]: authorization = request.headers.get("Authorization") if not authorization: if self.auto_error: - raise HTTPException( - status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" - ) + if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: + raise HTTPException( + status_code=HTTP_403_FORBIDDEN, detail="Not authenticated" + ) + else: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, + detail="Not authenticated", + headers={"WWW-Authenticate": "Bearer"}, + ) + else: return None return authorization diff --git a/tests/test_security_oauth2.py b/tests/test_security_oauth2.py index 2b7e3457a..804e4152d 100644 --- a/tests/test_security_oauth2.py +++ b/tests/test_security_oauth2.py @@ -56,8 +56,9 @@ def test_security_oauth2_password_other_header(): def test_security_oauth2_password_bearer_no_header(): response = client.get("/users/me") - assert response.status_code == 403, response.text + assert response.status_code == 401, response.text assert response.json() == {"detail": "Not authenticated"} + assert response.headers["WWW-Authenticate"] == "Bearer" def test_strict_login_no_data(): diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index 29866bfd4..6d6e904ae 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -4,6 +4,7 @@ import pytest from fastapi import FastAPI, Security from fastapi.security.api_key import APIKeyBase, APIKeyCookie, APIKeyHeader, APIKeyQuery from fastapi.security.http import HTTPBase, HTTPBearer, HTTPDigest +from fastapi.security.oauth2 import OAuth2 from fastapi.security.open_id_connect_url import OpenIdConnect from fastapi.testclient import TestClient @@ -65,6 +66,10 @@ def test_apikey_status_code_403_on_auth_error_no_auto_error(auth: APIKeyBase): [ HTTPBearer(not_authenticated_status_code=403), OpenIdConnect(not_authenticated_status_code=403, openIdConnectUrl="/openid"), + OAuth2( + not_authenticated_status_code=403, + flows={"password": {"tokenUrl": "token", "scopes": {}}}, + ), ], ) def test_oauth2_status_code_403_on_auth_error(auth: Union[HTTPBase, OpenIdConnect]): @@ -95,6 +100,12 @@ def test_oauth2_status_code_403_on_auth_error(auth: Union[HTTPBase, OpenIdConnec openIdConnectUrl="/openid", auto_error=False, ), + OAuth2( + not_authenticated_status_code=403, + flows={"password": {"tokenUrl": "token", "scopes": {}}}, + auto_error=False, + ), + ], ) def test_oauth2_status_code_403_on_auth_error_no_auto_error( From 2c7b3811fb6731f0ce7f19a05cd60154cde2af25 Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 19:12:11 +0200 Subject: [PATCH 10/12] Fix import of `Literal` --- fastapi/security/api_key.py | 4 ++-- fastapi/security/http.py | 4 ++-- fastapi/security/open_id_connect_url.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fastapi/security/api_key.py b/fastapi/security/api_key.py index b1388f92c..a6f4e85d0 100644 --- a/fastapi/security/api_key.py +++ b/fastapi/security/api_key.py @@ -1,11 +1,11 @@ -from typing import Literal, Optional, Union +from typing import Optional, Union from fastapi.openapi.models import APIKey, APIKeyIn from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, Doc, Literal, deprecated class APIKeyBase(SecurityBase): diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 5a550ff35..991101982 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -1,6 +1,6 @@ import binascii from base64 import b64decode -from typing import Literal, Optional +from typing import Optional from fastapi.exceptions import HTTPException from fastapi.openapi.models import HTTPBase as HTTPBaseModel @@ -10,7 +10,7 @@ from fastapi.security.utils import get_authorization_scheme_param from pydantic import BaseModel from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, Doc, Literal, deprecated class HTTPBasicCredentials(BaseModel): diff --git a/fastapi/security/open_id_connect_url.py b/fastapi/security/open_id_connect_url.py index c6b961ac0..76d91941e 100644 --- a/fastapi/security/open_id_connect_url.py +++ b/fastapi/security/open_id_connect_url.py @@ -1,11 +1,11 @@ -from typing import Literal, Optional +from typing import Optional from fastapi.openapi.models import OpenIdConnect as OpenIdConnectModel from fastapi.security.base import SecurityBase from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.status import HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN -from typing_extensions import Annotated, Doc, deprecated +from typing_extensions import Annotated, Doc, Literal, deprecated class OpenIdConnect(SecurityBase): From 173730108d5ae6407aa37b5435e7507074dcf00d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 17:45:50 +0000 Subject: [PATCH 11/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_security_status_code_403_option.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_security_status_code_403_option.py b/tests/test_security_status_code_403_option.py index 6d6e904ae..95d385ae4 100644 --- a/tests/test_security_status_code_403_option.py +++ b/tests/test_security_status_code_403_option.py @@ -105,7 +105,6 @@ def test_oauth2_status_code_403_on_auth_error(auth: Union[HTTPBase, OpenIdConnec flows={"password": {"tokenUrl": "token", "scopes": {}}}, auto_error=False, ), - ], ) def test_oauth2_status_code_403_on_auth_error_no_auto_error( From b1b257837931d8668a9d2c067de60f8769ed92ce Mon Sep 17 00:00:00 2001 From: Yurii Motov Date: Wed, 11 Jun 2025 20:07:50 +0200 Subject: [PATCH 12/12] Fix mypy warnings --- fastapi/security/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/security/http.py b/fastapi/security/http.py index 991101982..7d106fbe1 100644 --- a/fastapi/security/http.py +++ b/fastapi/security/http.py @@ -355,7 +355,7 @@ class HTTPBearer(HTTPBase): return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) - def _raise_not_authenticated_error(self, error_message: str): + def _raise_not_authenticated_error(self, error_message: str) -> None: if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=error_message) else: @@ -492,7 +492,7 @@ class HTTPDigest(HTTPBase): return None return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) - def _raise_not_authenticated_error(self, error_message: str): + def _raise_not_authenticated_error(self, error_message: str) -> None: if self.not_authenticated_status_code == HTTP_403_FORBIDDEN: raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail=error_message) else: