Newer
Older
gn-synapse / app / auth / stores.py
"""Redis-хранилища state/PKCE для OAuth-потока (TTL = срок state в gnexus-gauth).

Redis вместо in-memory: переживает рестарт api-контейнера и
работает при нескольких воркерах uvicorn/gunicorn.
"""

import json
from datetime import datetime

from redis import Redis

from app.config import get_settings

STATE_PREFIX = "synapse:auth:state:"
PKCE_PREFIX = "synapse:auth:pkce:"

_redis: Redis | None = None


def get_redis() -> Redis:
    global _redis
    if _redis is None:
        _redis = Redis.from_url(get_settings().redis_url, decode_responses=True)
    return _redis


def _ttl(expires_at: datetime) -> int:
    return max(1, int(expires_at.timestamp() - datetime.now(expires_at.tzinfo).timestamp()))


class RedisStateStore:
    """StateStoreInterface: state -> context (return_to, scopes)."""

    def put(self, state: str, expires_at: datetime, context: dict | None = None) -> None:
        get_redis().setex(STATE_PREFIX + state, _ttl(expires_at), json.dumps(context or {}))

    def has(self, state: str) -> bool:
        return bool(get_redis().exists(STATE_PREFIX + state))

    def get_context(self, state: str) -> dict:
        raw = get_redis().get(STATE_PREFIX + state)
        return json.loads(raw) if raw else {}

    def forget(self, state: str) -> None:
        get_redis().delete(STATE_PREFIX + state)


class RedisPkceStore:
    """PkceStoreInterface: state -> PKCE verifier."""

    def put(self, state: str, verifier: str, expires_at: datetime) -> None:
        get_redis().setex(PKCE_PREFIX + state, _ttl(expires_at), verifier)

    def get(self, state: str) -> str | None:
        return get_redis().get(PKCE_PREFIX + state)

    def forget(self, state: str) -> None:
        get_redis().delete(PKCE_PREFIX + state)