diff --git a/src/langbot/pkg/api/http/service/user.py b/src/langbot/pkg/api/http/service/user.py index fa1a68ad9..4be7c9466 100644 --- a/src/langbot/pkg/api/http/service/user.py +++ b/src/langbot/pkg/api/http/service/user.py @@ -445,6 +445,12 @@ class UserService: return jwt.encode(payload, jwt_secret, algorithm='HS256') def get_admin_owner_scope(self, token: str) -> dict[str, str] | None: + # Authentication already ran. Decode only to detect whether this optional + # claim exists, preserving bounded legacy tokens that omit iss/aud. + unverified = jwt.decode(token, options={'verify_signature': False}) + if unverified.get('admin_owner_scope') is None: + return None + jwt_secret = self.ap.instance_config.data['system']['jwt']['secret'] issuer, audience = self._jwt_identity() payload = jwt.decode( @@ -456,8 +462,6 @@ class UserService: options={'require': ['exp', 'iss', 'aud']}, ) scope = payload.get('admin_owner_scope') - if scope is None: - return None if not isinstance(scope, dict) or set(scope) != {'actor_account_uuid', 'workspace_uuid', 'effective_role'}: raise ValueError('Invalid admin owner token scope') if scope.get('effective_role') != 'owner': diff --git a/tests/unit_tests/api/service/test_user_service.py b/tests/unit_tests/api/service/test_user_service.py index 458e0644e..38f9440cb 100644 --- a/tests/unit_tests/api/service/test_user_service.py +++ b/tests/unit_tests/api/service/test_user_service.py @@ -448,6 +448,16 @@ class TestUserServiceGenerateJwtToken: 'effective_role': 'owner', } + legacy_token = jwt.encode( + { + 'user': 'legacy@example.com', + 'exp': datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=5), + }, + 'test_secret', + algorithm='HS256', + ) + assert service.get_admin_owner_scope(legacy_token) is None + async def test_admin_owner_token_rejects_invalid_scope(self): ap = SimpleNamespace() ap.instance_config = SimpleNamespace()