import logging import time import httpx logger = logging.getLogger(__name__) _DEFAULT_TOKEN_CACHE_TTL = 300 class AlexaSecurity: VALID_POLICIES = {"open", "pairing", "allowlist", "disabled"} def __init__(self, account): self._account = account self._policy = account.dm_policy if account.dm_policy in self.VALID_POLICIES else "open" self._allowlist: set[str] = set(account.allow_from) if account.allow_from else set() self._account_linking_enabled = getattr(account, "account_linking_enabled", False) self._oauth_client_id = getattr(account, "oauth_client_id", "") self._oauth_client_secret = getattr(account, "oauth_client_secret", "") self._oauth_introspect_url = getattr(account, "oauth_introspect_url", "") self._token_cache: dict[str, dict] = {} self._http = httpx.Client(timeout=httpx.Timeout(10.0)) @property def account_linking_enabled(self) -> bool: return self._account_linking_enabled def resolve_dm_policy(self) -> str: return self._policy def check_allowlist(self, user_id: str) -> bool: if self._policy == "open": return True if self._policy == "disabled": return False if not self._allowlist: return False return user_id in self._allowlist def add_to_allowlist(self, user_id: str): self._allowlist.add(user_id) def remove_from_allowlist(self, user_id: str): self._allowlist.discard(user_id) def verify_access_token(self, access_token: str) -> dict: if not access_token: return {"user_id": None, "verified": False} cached = self._token_cache.get(access_token) if cached and cached.get("expires_at", 0) > time.time(): return cached result = self._do_verify(access_token) result["expires_at"] = result.get("expires_at", time.time() + _DEFAULT_TOKEN_CACHE_TTL) self._token_cache[access_token] = result return result def _do_verify(self, access_token: str) -> dict: if self._oauth_introspect_url: return self._verify_via_introspect(access_token) return self._verify_via_jwt(access_token) def _verify_via_introspect(self, access_token: str) -> dict: try: resp = self._http.post( self._oauth_introspect_url, json={"token": access_token}, headers={"Authorization": f"Bearer {self._oauth_client_secret}"}, ) if resp.status_code == 200: data = resp.json() return { "user_id": data.get("sub") or data.get("user_id"), "verified": data.get("active", False), } logger.warning("OAuth introspect returned %d", resp.status_code) except Exception: logger.exception("OAuth token introspection failed") return {"user_id": None, "verified": False} def _verify_via_jwt(self, access_token: str) -> dict: try: from server.utils.auth_utils import AuthUtils payload = AuthUtils.verify_access_token(access_token) user_id = payload.get("sub") if user_id: return {"user_id": user_id, "verified": True} except Exception: logger.exception("JWT token verification failed") return {"user_id": None, "verified": False} def close(self): if self._http: self._http.close()