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"