From a42b126357cb3ba963b71de940b5354d9f7903ad Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Mon, 7 Sep 2026 12:38:46 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(backend)=20reject=20inact?= =?UTF-8?q?ive=20users=20in=20resource=20server=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resource server backend returned any user matching the token's `sub` claim without checking `User.is_active`. The upstream lasuite backend only validates the token's introspection `active` claim, so a deactivated Django account kept API access until its token expired. Raise `SuspiciousOperation` in `get_or_create_user` when the user is inactive, which the authentication class turns into a 401, consistent with `BaseJWTAuthentication`. Add unit and end-to-end tests. --- CHANGELOG.md | 1 + .../core/external_api/authentication.py | 4 + .../tests/test_external_api_authentication.py | 96 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/backend/core/tests/test_external_api_authentication.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e03a49..22817633 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to - 🐛(frontend) keep the sending resolution picked while the camera is off #1667 - 🐛(frontend) restore automatic lower-hand on speaking - 🐛(frontend) center Avatar initials with a font-aware cap-height ratio +- 🔒️(backend) reject inactive users in resource server backend ## [1.30.0] - 2026-09-01 diff --git a/src/backend/core/external_api/authentication.py b/src/backend/core/external_api/authentication.py index 496624f3..445eb02f 100644 --- a/src/backend/core/external_api/authentication.py +++ b/src/backend/core/external_api/authentication.py @@ -286,6 +286,10 @@ class ResourceServerBackend(LaSuiteBackend): if user is None and settings.OIDC_CREATE_USER: user = self.create_user(sub) + if user is not None and not user.is_active: + logger.warning("Inactive user attempted authentication: %s", user.pk) + raise SuspiciousOperation("User account is disabled.") + return user def create_user(self, sub): diff --git a/src/backend/core/tests/test_external_api_authentication.py b/src/backend/core/tests/test_external_api_authentication.py new file mode 100644 index 00000000..d3191f10 --- /dev/null +++ b/src/backend/core/tests/test_external_api_authentication.py @@ -0,0 +1,96 @@ +"""Tests for the external API ResourceServerBackend.""" + +from django.core.exceptions import SuspiciousOperation + +import pytest +import responses +from rest_framework.test import APIClient + +from core.external_api.authentication import ResourceServerBackend +from core.factories import UserFactory +from core.models import User + +pytestmark = pytest.mark.django_db + + +def _payload(sub): + return {"sub": sub, "active": True, "scope": "lasuite_meet", "client_id": "app"} + + +def test_resource_server_backend_get_or_create_user_active(): + """An existing active user matching the sub should be returned.""" + + user = UserFactory() + + result = ResourceServerBackend().get_or_create_user( + access_token="token", id_token=None, payload=_payload(user.sub) + ) + + assert result == user + + +def test_resource_server_backend_get_or_create_user_inactive(): + """An inactive user should be rejected even with a valid token.""" + + user = UserFactory(is_active=False) + + with pytest.raises(SuspiciousOperation, match="User account is disabled."): + ResourceServerBackend().get_or_create_user( + access_token="token", id_token=None, payload=_payload(user.sub) + ) + + +def test_resource_server_backend_get_or_create_user_creates(settings): + """An unknown sub should create an active user when OIDC_CREATE_USER is set.""" + + settings.OIDC_CREATE_USER = True + + result = ResourceServerBackend().get_or_create_user( + access_token="token", id_token=None, payload=_payload("new-sub") + ) + + assert result.sub == "new-sub" + assert result.is_active is True + assert User.objects.filter(sub="new-sub").exists() + + +def test_resource_server_backend_get_or_create_user_no_creation(settings): + """An unknown sub should return None when OIDC_CREATE_USER is unset.""" + + settings.OIDC_CREATE_USER = False + + result = ResourceServerBackend().get_or_create_user( + access_token="token", id_token=None, payload=_payload("new-sub") + ) + + assert result is None + assert not User.objects.filter(sub="new-sub").exists() + + +@responses.activate +def test_api_rooms_list_resource_server_inactive_user(settings): + """End to end: a valid introspected token for an inactive user should get 401.""" + + settings.OIDC_OP_INTROSPECTION_ENDPOINT = "https://oidc.example.com/introspect" + settings.OIDC_OP_URL = "https://oidc.example.com" + + user = UserFactory(is_active=False) + + responses.add( + responses.POST, + "https://oidc.example.com/introspect", + json={ + "iss": "https://oidc.example.com", + "active": True, + "sub": user.sub, + "scope": "lasuite_meet", + "client_id": "app", + }, + ) + + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Bearer rs-token") + response = client.get("/external-api/v1.0/rooms/") + + assert response.status_code == 401 + assert "login failed" in str(response.data).lower()