Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion autosubmit_api/routers/v4/auth.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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(
Expand Down
68 changes: 68 additions & 0 deletions tests/test_endpoints_v4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
b"<cas:authenticationSuccess><cas:user>jdoe</cas:user>"
b"</cas:authenticationSuccess></cas:serviceResponse>",
HTTPStatus.OK,
"jdoe",
id="success",
),
pytest.param(
b"<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
b"<cas:authenticationFailure code='INVALID_TICKET'>"
b"Ticket not recognized</cas:authenticationFailure>"
b"</cas:serviceResponse>",
HTTPStatus.UNAUTHORIZED,
None,
id="failure",
),
pytest.param(
b"<html>Service unavailable",
HTTPStatus.UNAUTHORIZED,
None,
id="not_xml",
),
pytest.param(
b"<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
b"</cas:serviceResponse>",
HTTPStatus.UNAUTHORIZED,
None,
id="empty_service_response",
),
pytest.param(
b"<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>"
b"<cas:authenticationSuccess></cas:authenticationSuccess>"
b"</cas:serviceResponse>",
HTTPStatus.UNAUTHORIZED,
None,
id="success_without_user",
),
pytest.param(
b"<serviceResponse><authenticationSuccess><user>jdoe</user>"
b"</authenticationSuccess></serviceResponse>",
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"
Expand Down
Loading