File size: 2,843 Bytes
db4ba8d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
"""
TradeFlow AI — CEISA 4.0 OAuth 2.0 Token Manager (T-050)

Manages the OAuth 2.0 client_credentials token lifecycle for CEISA H2H.
  - Acquires token on first use
  - Caches in Redis with TTL = expires_in - 60s (safety margin)
  - Transparently refreshes on expiry

PRD: No bare os.getenv() — all config from settings.
"""
from __future__ import annotations

import logging
from typing import Any

import httpx

logger = logging.getLogger("services.ceisa_auth")

_REDIS_KEY = "ceisa:oauth_token"


class CEISAAuthClient:
    """Thread-safe CEISA OAuth 2.0 token manager backed by Redis."""

    def __init__(self, settings: Any) -> None:
        self._settings = settings
        self._redis_url = settings.REDIS_URL

    async def _get_redis(self):
        import redis.asyncio as aioredis
        return await aioredis.from_url(self._redis_url, decode_responses=True)

    async def get_access_token(self) -> str:
        """Return a valid access token, acquiring a new one if needed."""
        r = await self._get_redis()
        try:
            token = await r.get(_REDIS_KEY)
            if token:
                return token
        finally:
            await r.aclose()

        return await self._acquire_token()

    async def _acquire_token(self) -> str:
        """Acquire a new token from CEISA OAuth endpoint and cache in Redis."""
        token_url = f"{self._settings.CEISA_BASE_URL.rstrip('/')}/auth/token"
        try:
            async with httpx.AsyncClient(timeout=15.0) as client:
                resp = await client.post(
                    token_url,
                    data={
                        "grant_type": "client_credentials",
                        "client_id": self._settings.CEISA_CLIENT_ID,
                        "client_secret": self._settings.CEISA_CLIENT_SECRET.get_secret_value(),
                    },
                )
                resp.raise_for_status()
                data = resp.json()
        except httpx.HTTPStatusError as e:
            logger.error(f"CEISA token acquisition failed: {e.response.status_code}")
            raise
        except Exception as e:
            logger.error(f"CEISA token endpoint unreachable: {e}")
            raise

        token = data.get("access_token")
        expires_in = int(data.get("expires_in", 3600))
        ttl = max(expires_in - 60, 60)  # safety margin

        r = await self._get_redis()
        try:
            await r.setex(_REDIS_KEY, ttl, token)
        finally:
            await r.aclose()

        logger.info(f"CEISA access token acquired, TTL={ttl}s")
        return token

    async def invalidate(self) -> None:
        """Force token invalidation (e.g., after 401 response)."""
        r = await self._get_redis()
        try:
            await r.delete(_REDIS_KEY)
        finally:
            await r.aclose()