ForcePilot/backend/package/yuxi/channel/extensions/msteams/auth.py
Kris 94444ced96 feat(channel): 添加 Microsoft Teams 渠道扩展
新增 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: 类型定义
2026-05-21 11:28:42 +08:00

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