From 6e587dfa424109f0a1a06d6b1895c317f3a95861 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Mon, 12 Oct 2026 08:46:09 +0200 Subject: [PATCH 1/4] test: protected endpoints return 401 with a clear detail when the token is missing or invalid --- .../tests/api/routers/test_authenticate.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/carbonserver/tests/api/routers/test_authenticate.py b/carbonserver/tests/api/routers/test_authenticate.py index 61a3533dc..4e6df5265 100644 --- a/carbonserver/tests/api/routers/test_authenticate.py +++ b/carbonserver/tests/api/routers/test_authenticate.py @@ -176,3 +176,61 @@ async def test_revoke_token_exception(oidc_provider, mock_oidc_client): # Should not raise await oidc_provider.revoke_token("test-access-token") + + +EXPERIMENT = { + "name": "Run on AWS", + "description": "d", + "timestamp": "2021-04-04T08:43:00+02:00", + "country_name": "France", + "country_iso_code": "FRA", + "region": "france", + "on_cloud": True, + "cloud_provider": "aws", + "cloud_region": "eu-west-1a", + "project_id": "8edb03e1-9a28-452a-9c93-a3b6560136d7", +} + + +@pytest.fixture +def protected_client(monkeypatch): + """Full-ish app with the catch-all 500 handler, auth enabled, provider rejecting tokens.""" + from starlette.responses import JSONResponse + + from carbonserver.api.routers import experiments + from carbonserver.api.services import auth_service + + monkeypatch.setattr(auth_service.settings, "auth_provider", "oidc") + monkeypatch.setattr(auth_service.settings, "environment", "production") + container = ServerContainer() + container.wire(modules=[authenticate, experiments, auth_service]) + provider = MagicMock() + provider.get_user_info = AsyncMock(side_effect=Exception("expired")) + container.auth_provider.override(provider) + container.sign_up_service.override(MagicMock()) + container.user_service.override(MagicMock()) + container.experiment_service.override(MagicMock()) + app = FastAPI() + app.container = container + app.include_router(experiments.router) + + async def generic(request, exc): + return JSONResponse({"detail": "Generic error"}, status_code=500) + + app.add_exception_handler(Exception, generic) + yield TestClient(app, raise_server_exceptions=False) + container.unwire() + + +def test_protected_endpoint_without_token_returns_401(protected_client): + response = protected_client.post("/experiments", json=EXPERIMENT) + assert response.status_code == 401 + assert response.json() == {"detail": "Missing or invalid access token"} + + +def test_protected_endpoint_with_invalid_token_returns_401(protected_client): + response = protected_client.post( + "/experiments", json=EXPERIMENT, headers={"Authorization": "Bearer bad"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "JWKS validation of the token failed" From 86c327d81349bd505e17b0b012de0e6498cf29cc Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Mon, 12 Oct 2026 08:46:09 +0200 Subject: [PATCH 2/4] fix: name the problem in the 401 returned when the access token is missing --- carbonserver/carbonserver/api/services/auth_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/carbonserver/carbonserver/api/services/auth_service.py b/carbonserver/carbonserver/api/services/auth_service.py index d0c91a0bf..f68129e5e 100644 --- a/carbonserver/carbonserver/api/services/auth_service.py +++ b/carbonserver/carbonserver/api/services/auth_service.py @@ -4,7 +4,7 @@ import jwt from dependency_injector.wiring import Provide -from fastapi import Depends, HTTPException +from fastapi import Depends from fastapi.security import APIKeyCookie, HTTPBearer from carbonserver.api.errors import AuthenticationError @@ -82,7 +82,7 @@ async def __call__( self.auth_user = None self.db_user = None if self.error_if_not_found: - raise HTTPException(status_code=401, detail="Unauthorized") + raise AuthenticationError(detail="Missing or invalid access token") return self self.auth_user = await self._verify_token(token, auth_provider) From bded5b38d25cacd26d280c6083a43dd9e6ddea11 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Mon, 12 Oct 2026 08:49:14 +0200 Subject: [PATCH 3/4] test: auth check without a token should return a null user --- carbonserver/tests/api/routers/test_authenticate.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/carbonserver/tests/api/routers/test_authenticate.py b/carbonserver/tests/api/routers/test_authenticate.py index 4e6df5265..c8aaf2053 100644 --- a/carbonserver/tests/api/routers/test_authenticate.py +++ b/carbonserver/tests/api/routers/test_authenticate.py @@ -213,6 +213,7 @@ def protected_client(monkeypatch): app = FastAPI() app.container = container app.include_router(experiments.router) + app.include_router(authenticate.router) async def generic(request, exc): return JSONResponse({"detail": "Generic error"}, status_code=500) @@ -234,3 +235,15 @@ def test_protected_endpoint_with_invalid_token_returns_401(protected_client): ) assert response.status_code == 401 assert response.json()["detail"] == "JWKS validation of the token failed" + + +def test_auth_check_without_token_returns_null_user(protected_client): + # The real SignUpService.check_jwt_user(None) raises TypeError (None["sub"]); mimic it. + sign_up = MagicMock() + sign_up.check_jwt_user.side_effect = TypeError( + "'NoneType' object is not subscriptable" + ) + protected_client.app.container.sign_up_service.override(sign_up) + response = protected_client.get("/auth/check") + assert response.status_code == 200 + assert response.json() == {"user": None} From 69deb9cbbd942ced0fb4e16e33598b95514a9ae2 Mon Sep 17 00:00:00 2001 From: David Berenstein Date: Mon, 12 Oct 2026 08:49:15 +0200 Subject: [PATCH 4/4] fix: auth check returns a null user instead of crashing when no token is sent --- carbonserver/carbonserver/api/routers/authenticate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/carbonserver/carbonserver/api/routers/authenticate.py b/carbonserver/carbonserver/api/routers/authenticate.py index e6abe8481..3480a4931 100644 --- a/carbonserver/carbonserver/api/routers/authenticate.py +++ b/carbonserver/carbonserver/api/routers/authenticate.py @@ -34,6 +34,8 @@ def check_login( return user data or redirect to login screen null value if not logged in """ + if auth_user.auth_user is None: + return {"user": None} sign_up_service.check_jwt_user(auth_user.auth_user, create=True) return {"user": auth_user.auth_user}