Rename the core package to periscopex, update UI/docs/Docker/deploy defaults to periscope.michelebigi.it, and keep legacy version/storage key aliases so existing projects keep working. Co-authored-by: Cursor <cursoragent@cursor.com>
116 lines
3.1 KiB
Python
116 lines
3.1 KiB
Python
"""JWT verification for FastAPI (Clerk JWKS or local Periscope HS256)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import jwt
|
|
from fastapi import Request
|
|
|
|
from backend.config import settings
|
|
|
|
# JWKS cache
|
|
_jwks_client: jwt.PyJWKClient | None = None
|
|
_SKIP_PATHS = {
|
|
"/docs",
|
|
"/openapi.json",
|
|
"/redoc",
|
|
"/health",
|
|
"/api/billing/webhook",
|
|
"/api/auth/mode",
|
|
"/api/auth/register",
|
|
"/api/auth/login",
|
|
"/api/contact",
|
|
}
|
|
|
|
|
|
def _get_jwks_client() -> jwt.PyJWKClient:
|
|
global _jwks_client
|
|
if _jwks_client is None:
|
|
jwks_url = settings.clerk_jwks_url
|
|
if not jwks_url:
|
|
raise RuntimeError(
|
|
"CLERK_JWKS_URL must be set for Clerk authentication. "
|
|
"Find it in your Clerk dashboard under API Keys."
|
|
)
|
|
_jwks_client = jwt.PyJWKClient(jwks_url, cache_keys=True)
|
|
return _jwks_client
|
|
|
|
|
|
def _bearer_or_query_token(request: Request) -> str | None:
|
|
auth_header = request.headers.get("authorization", "")
|
|
if auth_header.startswith("Bearer "):
|
|
return auth_header[7:]
|
|
# EventSource/SSE can't send headers
|
|
return request.query_params.get("token")
|
|
|
|
|
|
async def verify_clerk_token(request: Request) -> str | None:
|
|
"""Verify Clerk JWT and return user_id, or None if invalid."""
|
|
if request.url.path in _SKIP_PATHS:
|
|
return "anonymous"
|
|
|
|
token = _bearer_or_query_token(request)
|
|
if not token:
|
|
return None
|
|
|
|
try:
|
|
client = _get_jwks_client()
|
|
signing_key = client.get_signing_key_from_jwt(token)
|
|
|
|
payload: dict[str, Any] = jwt.decode(
|
|
token,
|
|
signing_key.key,
|
|
algorithms=["RS256"],
|
|
options={
|
|
"verify_exp": True,
|
|
"verify_aud": False,
|
|
"verify_iss": True,
|
|
},
|
|
issuer=(
|
|
settings.clerk_jwks_url.replace("/.well-known/jwks.json", "")
|
|
if settings.clerk_jwks_url
|
|
else None
|
|
),
|
|
leeway=10,
|
|
)
|
|
|
|
user_id = payload.get("sub")
|
|
if not user_id:
|
|
return None
|
|
return user_id
|
|
|
|
except jwt.ExpiredSignatureError:
|
|
return None
|
|
except jwt.InvalidTokenError:
|
|
return None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def verify_local_token(request: Request) -> str | None:
|
|
"""Verify Periscope local JWT and return user_id, or None if invalid."""
|
|
if request.url.path in _SKIP_PATHS:
|
|
return "anonymous"
|
|
|
|
token = _bearer_or_query_token(request)
|
|
if not token:
|
|
return None
|
|
|
|
from backend.services.local_jwt import decode_token
|
|
|
|
payload = decode_token(token)
|
|
if not payload:
|
|
return None
|
|
user_id = payload.get("sub")
|
|
return str(user_id) if user_id else None
|
|
|
|
|
|
async def verify_request_user(request: Request) -> str | None:
|
|
"""Dispatch to Clerk or local JWT verification."""
|
|
if settings.use_clerk:
|
|
return await verify_clerk_token(request)
|
|
if settings.use_local_auth:
|
|
return await verify_local_token(request)
|
|
return None
|