From 5d6ad3f3f634f343c64c633d50adf06e1a7835a7 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sun, 8 Feb 2026 21:40:25 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(backend)=20enhance=20scop?= =?UTF-8?q?e=20manipulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhance scope manipulation by normalizing and sanitizing scope values before processing. Scopes are now converted to lowercase to ensure consistent behavior, deduplicated while preserving their original order, and handled in a deterministic way aligned with the intended authorization model. --- src/backend/core/external_api/permissions.py | 3 ++ .../core/tests/test_external_api_rooms.py | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/backend/core/external_api/permissions.py b/src/backend/core/external_api/permissions.py index 82315b6e..1bf29e81 100644 --- a/src/backend/core/external_api/permissions.py +++ b/src/backend/core/external_api/permissions.py @@ -57,6 +57,9 @@ class BaseScopePermission(permissions.BasePermission): if isinstance(token_scopes, str): token_scopes = token_scopes.split() + # Ensure scopes is a deduplicated list (preserving order) and lowercase all scopes + token_scopes = list(dict.fromkeys(scope.lower() for scope in token_scopes)) + if settings.OIDC_RS_SCOPES_PREFIX: token_scopes = [ scope.removeprefix(f"{settings.OIDC_RS_SCOPES_PREFIX}:") diff --git a/src/backend/core/tests/test_external_api_rooms.py b/src/backend/core/tests/test_external_api_rooms.py index 00071585..eaf69ea1 100644 --- a/src/backend/core/tests/test_external_api_rooms.py +++ b/src/backend/core/tests/test_external_api_rooms.py @@ -660,6 +660,37 @@ def test_api_rooms_response_no_telephony(settings): assert response.data["id"] == str(room.id) +def test_api_rooms_token_scope_case_insensitive(settings): + """Token's scope should be case-insensitive.""" + settings.APPLICATION_JWT_SECRET_KEY = "devKey" + user = UserFactory() + + # Generate token with mixed-case scope "Rooms:List" to verify that scope + # validation is case-insensitive (should match "rooms:list") + now = datetime.now(timezone.utc) + payload = { + "iss": settings.APPLICATION_JWT_ISSUER, + "aud": settings.APPLICATION_JWT_AUDIENCE, + "iat": now, + "exp": now + timedelta(hours=1), + "client_id": "test-client", + "scope": "Rooms:List", # Mixed case - should be accepted as "rooms:list" + "user_id": str(user.id), + "delegated": True, + } + token = jwt.encode( + payload, + settings.APPLICATION_JWT_SECRET_KEY, + algorithm=settings.APPLICATION_JWT_ALG, + ) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") + response = client.get("/external-api/v1.0/rooms/") + + assert response.status_code == 200 + + def test_api_rooms_token_without_delegated_flag(settings): """Token without delegated flag should be rejected.""" settings.APPLICATION_JWT_SECRET_KEY = "devKey"