新增 Microsoft Teams 渠道扩展,支持在 Yuxi 平台中集成 Microsoft Teams 协作平台。 包含以下功能模块: - sdk: Bot Framework SDK 封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - auth: JWT 认证 - jwks: JWKS 密钥管理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - state: 状态管理 - runtime: 运行时管理 - actions: 动作处理 - adaptive_card: 自适应卡片 - task_modules: 任务模块 - message_extension: 消息扩展 - proactive: Proactive Messaging - graph: Microsoft Graph API 集成 - graph_teams: Teams 操作 - graph_members: 成员管理 - graph_messages: 消息获取 - graph_thread: 线程管理 - graph_users: 用户管理 - graph_upload: 文件上传 - files: 文件处理 - file_consent: 文件授权 - conversations: 会话存储 - mentions: @提及处理 - threading: 线程管理 - reactions: 表情反应 - polls: 投票功能 - meetings: 会议集成 - feedback: 反馈处理 - sso: 单点登录 - deep_links: 深层链接 - incoming_webhook: 入站 Webhook - localization: 本地化 - user_agent: 用户代理 - sent_message_cache: 消息缓存 - types: 类型定义
100 lines
3.0 KiB
Python
100 lines
3.0 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
import jwt
|
|
from jwt import ExpiredSignatureError, InvalidAudienceError, InvalidIssuerError, InvalidTokenError
|
|
|
|
from .jwks import JWKSNetworkError, collect_all_issuers, get_jwks_public_key, resolve_jwks_uri_for_iss
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
JWT_LEEWAY = 300
|
|
|
|
|
|
class MSTeamsAuthError(Exception):
|
|
pass
|
|
|
|
|
|
class MSTeamsTokenExpiredError(MSTeamsAuthError):
|
|
pass
|
|
|
|
|
|
class MSTeamsInvalidTokenError(MSTeamsAuthError):
|
|
pass
|
|
|
|
|
|
async def verify_botframework_jwt(token: str, app_id: str, tenant_id: str) -> dict:
|
|
try:
|
|
unverified_header = jwt.get_unverified_header(token)
|
|
unverified_payload = jwt.decode(token, options={"verify_signature": False})
|
|
except Exception as e:
|
|
raise MSTeamsInvalidTokenError(f"Invalid JWT format: {e}") from e
|
|
|
|
kid = unverified_header.get("kid")
|
|
iss = unverified_payload.get("iss", "")
|
|
|
|
jwks_uri = resolve_jwks_uri_for_iss(iss)
|
|
if not jwks_uri:
|
|
raise MSTeamsInvalidTokenError(f"Unknown issuer: {iss}")
|
|
|
|
try:
|
|
public_key_pem = await get_jwks_public_key(jwks_uri, kid)
|
|
except JWKSNetworkError as e:
|
|
raise MSTeamsAuthError(f"JWKS network error: {e}") from e
|
|
except Exception as e:
|
|
raise MSTeamsInvalidTokenError(f"JWKS key not found for kid '{kid}': {e}") from e
|
|
|
|
audiences = [
|
|
app_id,
|
|
f"api://{app_id}",
|
|
"https://api.botframework.com",
|
|
]
|
|
|
|
valid_issuers = list(collect_all_issuers(tenant_id))
|
|
|
|
try:
|
|
payload = jwt.decode(
|
|
token,
|
|
public_key_pem,
|
|
algorithms=["RS256", "RS384", "RS512"],
|
|
audience=audiences,
|
|
issuer=valid_issuers,
|
|
options={"require": ["exp", "nbf", "aud", "iss"]},
|
|
leeway=JWT_LEEWAY,
|
|
)
|
|
except ExpiredSignatureError as e:
|
|
logger.warning("JWT token expired")
|
|
raise MSTeamsTokenExpiredError("Token expired") from e
|
|
except InvalidAudienceError as e:
|
|
logger.warning("JWT invalid audience")
|
|
raise MSTeamsInvalidTokenError("Invalid audience") from e
|
|
except InvalidIssuerError as e:
|
|
logger.warning("JWT invalid issuer")
|
|
raise MSTeamsInvalidTokenError("Invalid issuer") from e
|
|
except InvalidTokenError as e:
|
|
logger.warning("JWT validation failed: %s", e)
|
|
raise MSTeamsInvalidTokenError(f"Token validation failed: {e}") from e
|
|
|
|
aud_value = payload.get("aud", [])
|
|
if isinstance(aud_value, str):
|
|
aud_value = [aud_value]
|
|
if "https://api.botframework.com" in aud_value:
|
|
appid = payload.get("appid") or payload.get("azp")
|
|
if appid and str(appid) != app_id:
|
|
raise MSTeamsInvalidTokenError("appid claim mismatch")
|
|
|
|
return payload
|
|
|
|
|
|
def extract_bearer_token(authorization: str | None) -> str | None:
|
|
if not authorization:
|
|
return None
|
|
|
|
parts = authorization.split(" ")
|
|
if len(parts) == 2 and parts[0].lower() == "bearer":
|
|
token = parts[1].strip()
|
|
if token:
|
|
return token
|
|
return None
|