from __future__ import annotations import json import time import httpx from yuxi.utils.logging_config import logger _CERT_URL = "https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com" _CERT_CACHE_TTL_S = 600 _CERT_FETCH_TIMEOUT_S = 10.0 class GoogleChatCertCache: def __init__(self, ttl_s: int = _CERT_CACHE_TTL_S): self._ttl = ttl_s self._cache: dict[str, str] | None = None self._cache_at: float = 0.0 @property def is_valid(self) -> bool: return self._cache is not None and (time.monotonic() - self._cache_at) < self._ttl async def get_certs(self) -> dict[str, str]: if self.is_valid and self._cache is not None: return dict(self._cache) try: async with httpx.AsyncClient(timeout=_CERT_FETCH_TIMEOUT_S) as client: resp = await client.get(_CERT_URL) resp.raise_for_status() certs: dict[str, str] = resp.json() self._cache = certs self._cache_at = time.monotonic() logger.info(f"Google Chat cert cache refreshed: {len(certs)} certificates") return dict(certs) except Exception as e: logger.warning(f"Failed to fetch Google Chat certs: {e}") if self._cache is not None: logger.info("Using stale cert cache") return dict(self._cache) return {} def clear(self) -> None: self._cache = None self._cache_at = 0.0 def verify_project_number_token(token: str, project_number: str, certs: dict[str, str]) -> bool: try: from google.auth import jwt except ImportError: logger.warning("google-auth not installed, skipping project-number token verification") return True if not certs: logger.warning("No certificates available for project-number verification") return False certs_json = json.dumps(certs) try: payload = jwt.decode(token, certs=certs_json, audience=project_number) issuer = payload.get("iss", "") expected_issuer = "chat@system.gserviceaccount.com" if issuer != expected_issuer: logger.warning(f"project-number token issuer mismatch: expected '{expected_issuer}', got '{issuer}'") return False return True except Exception as e: logger.warning(f"project-number token verification failed: {e}") return False _global_cert_cache: GoogleChatCertCache | None = None def get_cert_cache() -> GoogleChatCertCache: global _global_cert_cache if _global_cert_cache is None: _global_cert_cache = GoogleChatCertCache() return _global_cert_cache