"""Gitea OAuth2 support. Config shape (in ~/.hb.yaml): oauth: gitea: url: https://git.example.com client_id: client_secret: Register a Gitea OAuth2 application at: Gitea → Settings → Applications → OAuth2 Set the redirect URI to: https:///login/oauth/gitea/callback """ import logging import secrets import time logger = logging.getLogger(__name__) STATE_TTL = 600 # 10 minutes # state_token -> expiry timestamp _states: dict[str, float] = {} def make_state() -> str: """Generate a CSRF state token, store it with TTL, and return it.""" _purge_states() token = secrets.token_hex(32) _states[token] = time.time() + STATE_TTL return token def validate_state(state: str) -> bool: """Return True if *state* is known and unexpired; always removes it.""" expiry = _states.pop(state, None) if expiry is None: return False return time.time() < expiry def _purge_states() -> None: now = time.time() expired = [k for k, exp in list(_states.items()) if exp < now] for k in expired: del _states[k] class OAuthError(Exception): """Raised when the OAuth2 flow fails for any reason.""" def _gitea_cfg(config: dict) -> dict: """Return the gitea sub-dict or {} if absent/incomplete.""" return config.get("oauth", {}).get("gitea", {}) def is_enabled(config: dict) -> bool: """Return True when all three required Gitea OAuth keys are present.""" g = _gitea_cfg(config) return bool(g.get("url") and g.get("client_id") and g.get("client_secret"))