xtc-backend / app /auth.py
a3216's picture
deploy: initial release to Hugging Face Spaces
36fae79
Raw
History Blame Contribute Delete
6.52 kB
"""鉴权:access_key 校验 + JWT 临时令牌。
简化点:
- 去掉 XTC_ACCESS_SHORT_KEY / XTC_ACCESS_PIN
- 用 JWT 替代内存 Map,多实例可用
- access_token 持久化到 SQLite(用于吊销/查询)
"""
from __future__ import annotations
import secrets
import time
from typing import Optional
import jwt
from fastapi import Header, Request
from .config import get_settings
from .database import get_conn
from .errors import HttpError
ALGORITHM = "HS256"
# ===== access_key 校验 =====
def _extract_access_key(
*,
authorization: Optional[str],
x_access_key: Optional[str],
) -> Optional[str]:
if x_access_key:
return x_access_key.strip()
if authorization:
auth = authorization.strip()
if auth.lower().startswith("bearer "):
return auth[7:].strip()
if auth.lower().startswith("basic "):
return auth[6:].strip()
return auth
return None
def verify_access_key(provided: Optional[str]) -> None:
settings = get_settings()
if not settings.has_access_key:
# 未配置 access_key 时放行(开发模式)
return
if not provided:
raise HttpError(
"Missing access key: provide x-xtc-access-key header or Authorization: Bearer <key>",
status=401,
code="unauthorized",
)
# 先校验长期 key
if _consteq(provided, settings.xtc_access_key):
return
# 再校验临时 token
if verify_access_token(provided):
return
raise HttpError(
"Invalid access key or token",
status=401,
code="unauthorized",
)
def verify_access_token(token: str) -> bool:
"""校验临时令牌:JWT 签名 + SQLite 未吊销 + 未过期。"""
settings = get_settings()
if not settings.has_jwt_secret:
return False
try:
payload = jwt.decode(token, settings.xtc_jwt_secret, algorithms=[ALGORITHM])
except jwt.PyJWTError:
return False
if payload.get("type") != "access":
return False
jti = payload.get("jti")
if not jti:
return False
with get_conn() as conn:
row = conn.execute(
"SELECT expires_at, revoked FROM access_tokens WHERE token = ?",
(jti,),
).fetchone()
if not row:
return False
if row["revoked"]:
return False
if int(row["expires_at"]) < int(time.time()):
return False
return True
# ===== 临时令牌签发 =====
def issue_access_token(*, issued_by: str = "admin") -> dict:
settings = get_settings()
if not settings.has_jwt_secret:
raise HttpError(
"XTC_JWT_SECRET not configured, cannot issue token",
status=500,
code="server_misconfigured",
)
ttl = settings.xtc_access_token_ttl_sec
length = settings.xtc_access_token_length
jti = secrets.token_urlsafe(length)[:length] if length <= 32 else secrets.token_urlsafe(32)
now = int(time.time())
expires_at = now + ttl
payload = {
"type": "access",
"jti": jti,
"iat": now,
"exp": expires_at,
}
token = jwt.encode(payload, settings.xtc_jwt_secret, algorithm=ALGORITHM)
with get_conn() as conn:
conn.execute(
"INSERT INTO access_tokens(token, issued_at, expires_at, issued_by, revoked) "
"VALUES(?,?,?,?,0)",
(jti, now, expires_at, issued_by),
)
return {
"token": token,
"jti": jti,
"issued_at": now,
"expires_at": expires_at,
"expires_in_sec": ttl,
}
def revoke_access_token(jti: str) -> bool:
with get_conn() as conn:
cur = conn.execute(
"UPDATE access_tokens SET revoked = 1 WHERE token = ? AND revoked = 0",
(jti,),
)
return cur.rowcount > 0
def list_access_tokens() -> list[dict]:
now = int(time.time())
with get_conn() as conn:
rows = conn.execute(
"SELECT token, issued_at, expires_at, issued_by, revoked FROM access_tokens "
"ORDER BY issued_at DESC LIMIT 200"
).fetchall()
out = []
for r in rows:
out.append(
{
"jti": r["token"],
"issued_at": int(r["issued_at"]),
"expires_at": int(r["expires_at"]),
"issued_by": r["issued_by"],
"revoked": bool(r["revoked"]),
"expired": int(r["expires_at"]) < now,
}
)
return out
# ===== admin key 校验 =====
def verify_admin_key(provided: Optional[str]) -> None:
settings = get_settings()
if not settings.is_admin_enabled:
raise HttpError(
"Admin API disabled (XTC_DISABLE_ADMIN=true)",
status=403,
code="forbidden",
)
if not settings.has_admin_key:
raise HttpError(
"XTC_ADMIN_KEY not configured",
status=500,
code="server_misconfigured",
)
if not provided:
raise HttpError(
"Missing admin key: provide x-xtc-admin-key header or Authorization: Bearer <key>",
status=401,
code="unauthorized",
)
if not _consteq(provided, settings.xtc_admin_key):
raise HttpError("Invalid admin key", status=401, code="unauthorized")
# ===== FastAPI 依赖 =====
def require_access_key(
request: Request,
authorization: Optional[str] = Header(default=None),
x_xtc_access_key: Optional[str] = Header(default=None, alias="x-xtc-access-key"),
x_xtc_access_token: Optional[str] = Header(default=None, alias="x-xtc-access-token"),
) -> str:
provided = _extract_access_key(
authorization=authorization,
x_access_key=x_xtc_access_key or x_xtc_access_token,
)
verify_access_key(provided)
return provided or ""
def require_admin_key(
request: Request,
authorization: Optional[str] = Header(default=None),
x_xtc_admin_key: Optional[str] = Header(default=None, alias="x-xtc-admin-key"),
) -> str:
provided = _extract_access_key(
authorization=authorization,
x_access_key=x_xtc_admin_key,
)
verify_admin_key(provided)
return provided or ""
def _consteq(a: str, b: str) -> bool:
if not isinstance(a, str) or not isinstance(b, str):
return False
if len(a) != len(b):
return False
result = 0
for x, y in zip(a, b):
result |= ord(x) ^ ord(y)
return result == 0