Browse Source

Fix `HTTPDigest` security scheme status code on "Not authenticated" error

pull/13786/head
Yurii Motov 1 month ago
parent
commit
51503835f3
  1. 47
      fastapi/security/http.py
  2. 6
      tests/test_security_http_digest.py
  3. 6
      tests/test_security_http_digest_description.py
  4. 40
      tests/test_security_status_code_403_option.py

47
fastapi/security/http.py

@ -430,10 +430,38 @@ class HTTPDigest(HTTPBase):
""" """
), ),
] = True, ] = 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.model = HTTPBaseModel(scheme="digest", description=description)
self.scheme_name = scheme_name or self.__class__.__name__ self.scheme_name = scheme_name or self.__class__.__name__
self.auto_error = auto_error self.auto_error = auto_error
self.not_authenticated_status_code = not_authenticated_status_code
async def __call__( async def __call__(
self, request: Request self, request: Request
@ -442,17 +470,24 @@ class HTTPDigest(HTTPBase):
scheme, credentials = get_authorization_scheme_param(authorization) scheme, credentials = get_authorization_scheme_param(authorization)
if not (authorization and scheme and credentials): if not (authorization and scheme and credentials):
if self.auto_error: if self.auto_error:
raise HTTPException( self._raise_not_authenticated_error(error_message="Not authenticated")
status_code=HTTP_403_FORBIDDEN, detail="Not authenticated"
)
else: else:
return None return None
if scheme.lower() != "digest": if scheme.lower() != "digest":
if self.auto_error: if self.auto_error:
raise HTTPException( self._raise_not_authenticated_error(
status_code=HTTP_403_FORBIDDEN, error_message="Invalid authentication credentials",
detail="Invalid authentication credentials",
) )
else: else:
return None return None
return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) 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"},
)

6
tests/test_security_http_digest.py

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/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.json() == {"detail": "Invalid authentication credentials"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

6
tests/test_security_http_digest_description.py

@ -23,16 +23,18 @@ def test_security_http_digest():
def test_security_http_digest_no_credentials(): def test_security_http_digest_no_credentials():
response = client.get("/users/me") 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.json() == {"detail": "Not authenticated"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_security_http_digest_incorrect_scheme_credentials(): def test_security_http_digest_incorrect_scheme_credentials():
response = client.get( response = client.get(
"/users/me", headers={"Authorization": "Other invalidauthorization"} "/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.json() == {"detail": "Invalid authentication credentials"}
assert response.headers["WWW-Authenticate"] == "Digest"
def test_openapi_schema(): def test_openapi_schema():

40
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("/") response = client.get("/")
assert response.status_code == 200 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

Loading…
Cancel
Save