FortyDN Dev
fix(auth): exact-match open paths for engine/status, metrics/inference, smtp/probe
5979e23 Download app/auth.py from sitcod3/modul-x: direct link, hf CLI and curl.
- Browser
- Download file 6.5 kB
-
https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/app/auth.py
- Command line
-
hf download hf://spaces/sitcod3/modul-x/app/auth.py
-
curl -L -o auth.py https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/app/auth.py
6.5 kB
| """Supabase JWT verification + license gating for the paid API. | |
| The central Supabase project issues the session JWT; this backend verifies | |
| it against the project's PUBLIC JWKS endpoint (no shared database needed). | |
| License rows are read from the central project via its service-role REST | |
| API (server-side only). | |
| Enforcement is OFF by default (LICENSE_ENFORCEMENT != "true") so existing | |
| open/demo deployments keep working. Turn it on at go-live: | |
| SUPABASE_URL=https://ciytvulujiqemqljablq.supabase.co | |
| SUPABASE_SERVICE_ROLE_KEY=<service role key, server only> | |
| LICENSE_ENFORCEMENT=true | |
| Frontend equivalent lives in the FortyDN website repo (src/lib/productApi.ts). | |
| Follows docs/AUTH_SETUP.md section 3 (Python snippet). | |
| """ | |
| import logging | |
| from typing import Optional | |
| import httpx | |
| from fastapi import Request | |
| from fastapi.responses import JSONResponse | |
| from jose import jwt as jose_jwt | |
| from jose import jwk as jose_jwk | |
| from jose.exceptions import JWTError | |
| from .config import settings | |
| logger = logging.getLogger("emailvalidator.auth") | |
| CENTRAL_SUPABASE_URL = ( | |
| settings.supabase_url or "https://ciytvulujiqemqljablq.supabase.co" | |
| ) | |
| JWKS_URL = f"{CENTRAL_SUPABASE_URL}/auth/v1/.well-known/jwks.json" | |
| # Modul-X is unlocked directly, via Validate+Send, or via Full Bridge. | |
| MODULX_GRANTS = frozenset( | |
| { | |
| "modul-x", | |
| "combo-validate-send", | |
| "combo-full-bridge", | |
| } | |
| ) | |
| _jwks_cache: Optional[dict] = None | |
| def enforcement_on() -> bool: | |
| return (settings.license_enforcement or "").lower() == "true" | |
| async def _get_jwks() -> dict: | |
| """Fetch (and cache) the Supabase JWKS document.""" | |
| global _jwks_cache | |
| if _jwks_cache is None: | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| res = await client.get(JWKS_URL) | |
| res.raise_for_status() | |
| _jwks_cache = res.json() | |
| return _jwks_cache # type: ignore[return-value] | |
| def _bearer_token(request: Request) -> Optional[str]: | |
| header = request.headers.get("authorization", "") | |
| if not header.startswith("Bearer "): | |
| return None | |
| token = header[7:].strip() | |
| return token if token else None | |
| async def verify_supabase_jwt(token: str) -> dict: | |
| """Verify the Supabase session JWT via JWKS. Returns claims (sub = user_id).""" | |
| jwks = await _get_jwks() | |
| # Supabase rotates keys; try each until one verifies. | |
| last_error: Optional[Exception] = None | |
| for key_data in jwks.get("keys", []): | |
| try: | |
| key = jose_jwk.construct(key_data) | |
| claims = jose_jwt.decode( | |
| token, | |
| key, | |
| algorithms=["ES256", "RS256"], | |
| audience="authenticated", | |
| ) | |
| if not claims.get("sub"): | |
| raise JWTError("JWT has no sub claim") | |
| return claims | |
| except JWTError as exc: | |
| last_error = exc | |
| continue | |
| raise JWTError(f"JWT verification failed: {last_error}") | |
| async def has_modulx_license(user_id: str) -> bool: | |
| """Check for an active Modul-X license via the central service-role API.""" | |
| service_key = settings.supabase_service_role_key | |
| if not service_key: | |
| logger.warning("[auth] SUPABASE_SERVICE_ROLE_KEY not set - cannot check licenses.") | |
| return False | |
| params = { | |
| "user_id": f"eq.{user_id}", | |
| "status": "eq.active", | |
| "select": "product", | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=10.0) as client: | |
| res = await client.get( | |
| f"{CENTRAL_SUPABASE_URL}/rest/v1/licenses", | |
| params=params, | |
| headers={ | |
| "apikey": service_key, | |
| "Authorization": f"Bearer {service_key}", | |
| }, | |
| ) | |
| if res.status_code != 200: | |
| # Missing table (migrations not applied yet) => fail closed with a hint. | |
| logger.warning( | |
| "[auth] license lookup HTTP %s: %s", | |
| res.status_code, | |
| res.text[:200], | |
| ) | |
| return False | |
| rows = res.json() | |
| return isinstance(rows, list) and any( | |
| isinstance(r, dict) and r.get("product") in MODULX_GRANTS for r in rows | |
| ) | |
| except Exception as exc: | |
| logger.warning("[auth] license lookup error: %s", exc) | |
| return False | |
| # Paths that stay open (health probes, docs, root) plus the autonomous-engine | |
| # ops surfaces: /engine/status + /metrics/inference are aggregate diagnostics | |
| # (no PII), and /smtp/probe* enforce their own shared-secret guard | |
| # (X-Worker-Token) — license JWTs do not apply to machine-to-machine probes. | |
| _OPEN_PREFIXES = ( | |
| "/api/v1/health", | |
| "/api/health", | |
| "/api/v1/engine/status", | |
| "/api/v1/metrics", | |
| "/api/v1/smtp/probe", | |
| "/docs", | |
| "/redoc", | |
| "/openapi.json", | |
| ) | |
| _OPEN_EXACT = frozenset( | |
| { | |
| "/", | |
| "/api/v1/health", | |
| "/api/health", | |
| "/api/v1/engine/status", | |
| "/api/v1/metrics/inference", | |
| "/api/v1/smtp/probe", | |
| } | |
| ) | |
| def _is_open_path(path: str) -> bool: | |
| if path in _OPEN_EXACT: | |
| return True | |
| return any(path.startswith(p) and path != p for p in _OPEN_PREFIXES) | |
| async def license_middleware(request: Request, call_next): | |
| """FastAPI HTTP middleware: JWT + license gate for paid routes. | |
| Mirrors the Express pattern (Modul-S/E): /api/v1/health stays open, | |
| everything else under /api requires a valid JWT + active license. | |
| No-op while LICENSE_ENFORCEMENT != "true" (demo mode). | |
| """ | |
| if not enforcement_on(): | |
| return await call_next(request) | |
| path = request.url.path | |
| if _is_open_path(path) or not path.startswith("/api"): | |
| return await call_next(request) | |
| token = _bearer_token(request) | |
| if not token: | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Missing bearer token - sign in first."}, | |
| ) | |
| try: | |
| claims = await verify_supabase_jwt(token) | |
| except JWTError: | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Invalid or expired token."}, | |
| ) | |
| user_id = claims.get("sub", "") | |
| if not user_id or not await has_modulx_license(user_id): | |
| return JSONResponse( | |
| status_code=402, | |
| content={"detail": "No active Modul-X license - see /pricing."}, | |
| ) | |
| request.state.auth = {"user_id": user_id, "email": claims.get("email")} | |
| return await call_next(request) | |