Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import asyncio | |
| import logging | |
| from src.config import settings | |
| logger = logging.getLogger(__name__) | |
| class RedisTokenDenylist: | |
| def __init__(self) -> None: | |
| self._redis = None | |
| self._lock = asyncio.Lock() | |
| self._warned_unavailable = False | |
| def _key(self, jti: str) -> str: | |
| return f"{settings.jwt_denylist_prefix}{jti}" | |
| def _warn_once(self, message: str) -> None: | |
| if self._warned_unavailable: | |
| return | |
| self._warned_unavailable = True | |
| logger.warning(message) | |
| async def _get_redis(self): | |
| if not settings.jwt_denylist_enabled: | |
| return None | |
| if self._redis is not None: | |
| return self._redis | |
| async with self._lock: | |
| if self._redis is not None: | |
| return self._redis | |
| try: | |
| from redis.asyncio import Redis | |
| except ImportError: | |
| self._warn_once("redis package missing; JWT denylist disabled.") | |
| return None | |
| redis = Redis.from_url( | |
| settings.redis_url, | |
| decode_responses=True, | |
| ) | |
| try: | |
| await redis.ping() | |
| except Exception: | |
| await redis.close() | |
| self._warn_once("Redis unavailable; JWT denylist checks skipped.") | |
| return None | |
| self._redis = redis | |
| return self._redis | |
| async def is_revoked(self, jti: str) -> bool: | |
| if not settings.jwt_denylist_enabled or not jti: | |
| return False | |
| redis = await self._get_redis() | |
| if redis is None: | |
| return False | |
| try: | |
| return bool(await redis.exists(self._key(jti))) | |
| except Exception: | |
| self._warn_once("Redis denylist read failed; revocation checks skipped.") | |
| return False | |
| async def revoke(self, jti: str, *, ttl_seconds: int | None = None) -> None: | |
| if not settings.jwt_denylist_enabled or not jti: | |
| return | |
| redis = await self._get_redis() | |
| if redis is None: | |
| return | |
| ttl = max(1, ttl_seconds or settings.oauth_token_ttl_seconds) | |
| try: | |
| await redis.set(self._key(jti), "1", ex=ttl) | |
| except Exception: | |
| self._warn_once("Redis denylist write failed; revoke skipped.") | |
| async def shutdown(self) -> None: | |
| if self._redis is None: | |
| return | |
| await self._redis.close() | |
| self._redis = None | |
| self._warned_unavailable = False | |
| token_denylist = RedisTokenDenylist() | |
| async def is_token_revoked(jti: str) -> bool: | |
| return await token_denylist.is_revoked(jti) | |
| async def revoke_token(jti: str, *, ttl_seconds: int | None = None) -> None: | |
| await token_denylist.revoke(jti, ttl_seconds=ttl_seconds) | |
| async def shutdown_token_denylist() -> None: | |
| await token_denylist.shutdown() | |