fix(hub): SSO in hub_sso without Flask for FastAPI hub
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
实例浏览器 SSO(复用 HUB_BRIDGE_TOKEN)。无 Flask 依赖,供中控 FastAPI 与各实例共用。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import threading
|
||||
import time
|
||||
|
||||
HUB_SSO_TTL_SEC = int(os.getenv("HUB_SSO_TTL_SEC", "7200"))
|
||||
|
||||
_used_nonces: dict[str, float] = {}
|
||||
_nonce_lock = threading.Lock()
|
||||
|
||||
|
||||
def hub_bridge_token() -> str:
|
||||
return (os.getenv("HUB_BRIDGE_TOKEN") or "").strip()
|
||||
|
||||
|
||||
def safe_next_path(raw: str | None) -> str:
|
||||
p = (raw or "/").strip()
|
||||
if not p.startswith("/") or p.startswith("//"):
|
||||
return "/"
|
||||
if "://" in p:
|
||||
return "/"
|
||||
return p
|
||||
|
||||
|
||||
def _sso_secret() -> str:
|
||||
return hub_bridge_token()
|
||||
|
||||
|
||||
def _b64url_encode(data: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(data).decode().rstrip("=")
|
||||
|
||||
|
||||
def _b64url_decode(data: str) -> bytes:
|
||||
pad = "=" * (-len(data) % 4)
|
||||
return base64.urlsafe_b64decode(data + pad)
|
||||
|
||||
|
||||
def _prune_used_nonces() -> None:
|
||||
now = time.time()
|
||||
with _nonce_lock:
|
||||
dead = [k for k, exp in _used_nonces.items() if exp <= now]
|
||||
for k in dead:
|
||||
del _used_nonces[k]
|
||||
|
||||
|
||||
def mint_hub_sso_token(exchange_key: str, next_path: str = "/") -> str | None:
|
||||
secret = _sso_secret()
|
||||
ex = (exchange_key or "").strip().lower()
|
||||
if not secret or not ex:
|
||||
return None
|
||||
payload = {
|
||||
"ex": ex,
|
||||
"exp": int(time.time()) + max(60, HUB_SSO_TTL_SEC),
|
||||
"nonce": secrets.token_urlsafe(16),
|
||||
"next": safe_next_path(next_path),
|
||||
}
|
||||
body = _b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
|
||||
sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
|
||||
return f"{body}.{sig}"
|
||||
|
||||
|
||||
def verify_hub_sso_token(
|
||||
token: str | None, expected_exchange: str
|
||||
) -> tuple[bool, str, str | None]:
|
||||
secret = _sso_secret()
|
||||
expected = (expected_exchange or "").strip().lower()
|
||||
if not secret or not expected:
|
||||
return False, "/", "未配置 HUB_BRIDGE_TOKEN"
|
||||
raw = (token or "").strip()
|
||||
if "." not in raw:
|
||||
return False, "/", "token 无效"
|
||||
body, sig = raw.rsplit(".", 1)
|
||||
try:
|
||||
expect_sig = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(expect_sig, sig):
|
||||
return False, "/", "签名校验失败"
|
||||
payload = json.loads(_b64url_decode(body).decode())
|
||||
except Exception:
|
||||
return False, "/", "token 解析失败"
|
||||
if not isinstance(payload, dict):
|
||||
return False, "/", "payload 无效"
|
||||
if str(payload.get("ex") or "").lower() != expected:
|
||||
return False, "/", "实例不匹配"
|
||||
try:
|
||||
exp = int(payload.get("exp") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return False, "/", "exp 无效"
|
||||
if exp < int(time.time()):
|
||||
return False, "/", "链接已过期"
|
||||
nonce = str(payload.get("nonce") or "")
|
||||
if not nonce:
|
||||
return False, "/", "nonce 缺失"
|
||||
_prune_used_nonces()
|
||||
with _nonce_lock:
|
||||
if nonce in _used_nonces:
|
||||
return False, "/", "链接已使用"
|
||||
_used_nonces[nonce] = float(exp)
|
||||
return True, safe_next_path(str(payload.get("next") or "/")), None
|
||||
Reference in New Issue
Block a user