diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d95a9f..3f3d9d73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Migrated `setup.py`, `pytest.ini`, `ruff.toml` and `.coveragerc` to `pyproject.toml` to allow Dependabot to discover outdated dependencies #350 * Tightened `requests` upper bound to `<2.33.0` matching the ceiling imposed by `autosubmit` v4.1.17 dependency (`requests<=2.32.5`) * `/v3/login` answers 401 instead of 500 when the CAS ticket verification response is not valid XML #383 +* `/v4/auth/cas/v2/login` returns 401 instead of 500 when the CAS server's ticket validation response is not a valid CAS document #383 ### Release v4.2.0 - Release date: 2026-09-28 diff --git a/autosubmit_api/routers/v4/auth.py b/autosubmit_api/routers/v4/auth.py index 4631724e..e2fa2271 100644 --- a/autosubmit_api/routers/v4/auth.py +++ b/autosubmit_api/routers/v4/auth.py @@ -1,4 +1,5 @@ from http import HTTPStatus +import xml.etree.ElementTree as ET from typing import Optional from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse, RedirectResponse @@ -8,6 +9,7 @@ from autosubmit_api.auth import ProtectionLevels, auth_token_dependency, oidc from autosubmit_api.auth.utils import validate_client from autosubmit_api.auth.utils import generate_jwt_token +from autosubmit_api.logger import logger from autosubmit_api.models.responses import AuthResponse, LoginResponse @@ -66,7 +68,11 @@ async def cas_v2_login( # There is a ticket, the request come from CAS as callback. # need call `verify_ticket()` to validate ticket and get user profile. - user, attributes, pgtiou = cas_client.verify_ticket(ticket) + try: + user, attributes, pgtiou = cas_client.verify_ticket(ticket) + except (ET.ParseError, IndexError, AttributeError, ValueError) as exc: + logger.warning("Unexpected CAS ticket validation response: %s", exc) + user = None if not user: return JSONResponse( diff --git a/tests/test_endpoints_v4.py b/tests/test_endpoints_v4.py index 7af69fa9..d2adc3f2 100644 --- a/tests/test_endpoints_v4.py +++ b/tests/test_endpoints_v4.py @@ -44,6 +44,74 @@ def test_invalid_client( response = fixture_fastapi_client.get(self.endpoint, params={"service": "asd"}) assert response.status_code == HTTPStatus.UNAUTHORIZED + @pytest.mark.parametrize( + "cas_response, expected_status, expected_user", + [ + pytest.param( + b"" + b"jdoe" + b"", + HTTPStatus.OK, + "jdoe", + id="success", + ), + pytest.param( + b"" + b"" + b"Ticket not recognized" + b"", + HTTPStatus.UNAUTHORIZED, + None, + id="failure", + ), + pytest.param( + b"Service unavailable", + HTTPStatus.UNAUTHORIZED, + None, + id="not_xml", + ), + pytest.param( + b"" + b"", + HTTPStatus.UNAUTHORIZED, + None, + id="empty_service_response", + ), + pytest.param( + b"" + b"" + b"", + HTTPStatus.UNAUTHORIZED, + None, + id="success_without_user", + ), + pytest.param( + b"jdoe" + b"", + HTTPStatus.UNAUTHORIZED, + None, + id="no_namespace", + ), + ], + ) + def test_ticket_validation( + self, + fixture_fastapi_client: TestClient, + monkeypatch: pytest.MonkeyPatch, + cas_response: bytes, + expected_status: HTTPStatus, + expected_user: str | None, + ): + monkeypatch.setattr( + "cas.CASClientV2.get_verification_response", + lambda self, ticket: cas_response, + ) + response = fixture_fastapi_client.get( + self.endpoint, params={"ticket": "ST-1-test"} + ) + assert response.status_code == expected_status + assert response.json()["user"] == expected_user + class TestOIDCLogin: endpoint = "/v4/auth/oidc/login"