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