Files
agent-radar 1a3a4ccde1
Creator App Tests / PR Spam Gate (push) Skipped
Frontend Tests / PR Spam Gate (push) Skipped
NPM Format (website & console) / PR Spam Gate (push) Skipped
Pre-commit Checks / PR Spam Gate (push) Skipped
QwenPaw Creator Plugin Release / check-version (push) Canceled after 0s
QwenPaw Creator Plugin Release / upload-oss (push) Canceled after 0s
Tests / PR Spam Gate (push) Skipped
CodeQL / CodeQL (python) (push) Canceled after 0s
CodeQL / CodeQL (typescript) (push) Canceled after 0s
Creator App Tests / Creator Backend Tests (push) Canceled after 0s
Creator App Tests / Creator UI Build & Tests (push) Canceled after 0s
E2E Smoke Tests / Playwright UI Smoke (push) Canceled after 0s
Frontend Tests / Detect console changes (push) Canceled after 0s
NPM Format (website & console) / website format check (push) Canceled after 0s
NPM Format (website & console) / console format check (push) Canceled after 0s
Pre-commit Checks / Pre-commit (push) Canceled after 0s
QwenPaw Data Plugin Release / check-version (push) Canceled after 0s
Tests / Detect code changes (push) Canceled after 0s
Frontend Tests / Vitest Unit Tests (push) Canceled after 0s
Frontend Tests / Frontend Summary (push) Canceled after 0s
QwenPaw Data Plugin Release / upload-oss (push) Canceled after 0s
Tests / Maintainer Approval (push) Canceled after 0s
Tests / Unit Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Unit Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - macos-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.13 - ubuntu-latest (push) Canceled after 0s
Tests / Contract Tests - py3.11 - windows-latest (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - macos-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - fallback (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.13 - ubuntu-latest - p2 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p0 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p1 (push) Canceled after 0s
Tests / Integrated Tests - py3.11 - windows-latest - p2 (push) Canceled after 0s
Tests / Coverage Report (push) Canceled after 0s
Tests / Test Summary (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:01:50 +08:00

168 lines
4.9 KiB
Python

# -*- coding: utf-8 -*-
"""JWT token validation for inbound Azure Bot Service requests."""
from __future__ import annotations
import logging
import time
from typing import Optional
import aiohttp
import jwt
from jwt import PyJWKClient
from .constants import (
AZURE_BOT_JWKS_CACHE_TTL_S,
AZURE_BOT_OPENID_METADATA_URL,
)
logger = logging.getLogger(__name__)
class AzureBotTokenValidator:
"""Validate Bearer tokens on inbound Bot Framework requests.
Bot Framework sends a JWT signed by Microsoft's identity
platform. We verify:
- Signature (via JWKS from OpenID metadata)
- Issuer matches Bot Framework issuers
- Audience matches our app_id
"""
# Known Bot Framework token issuers
_VALID_ISSUERS = (
"https://api.botframework.com",
"https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
"https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0", # noqa: E501
)
def __init__(
self,
app_id: str,
tenant_id: str = "",
) -> None:
self._app_id = app_id
self._tenant_id = tenant_id
self._jwks_client: Optional[PyJWKClient] = None
self._jwks_uri: Optional[str] = None
self._jwks_fetched_at: float = 0.0
self._openid_config: Optional[dict] = None
async def _ensure_jwks_client(self) -> None:
"""Lazily fetch OpenID metadata and create JWKS client."""
now = time.time()
if (
self._jwks_client is not None
and (now - self._jwks_fetched_at) < AZURE_BOT_JWKS_CACHE_TTL_S
):
return
try:
async with aiohttp.ClientSession() as session:
async with session.get(
AZURE_BOT_OPENID_METADATA_URL,
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.status != 200:
logger.warning(
"azure_bot auth: failed to fetch "
"OpenID metadata status=%d",
resp.status,
)
return
self._openid_config = await resp.json()
jwks_uri = self._openid_config.get(
"jwks_uri",
"",
)
if not jwks_uri:
logger.warning(
"azure_bot auth: no jwks_uri in OpenID metadata",
)
return
self._jwks_uri = jwks_uri
self._jwks_client = PyJWKClient(jwks_uri)
self._jwks_fetched_at = now
logger.debug(
"azure_bot auth: JWKS client initialized from %s",
jwks_uri,
)
except Exception:
logger.exception(
"azure_bot auth: error fetching OpenID metadata",
)
async def validate_auth_header(
self,
auth_header: str,
) -> bool:
"""Validate the Authorization header.
Args:
auth_header: Full "Bearer <token>" header value.
Returns:
True if the token is valid, False otherwise.
"""
if not auth_header or not auth_header.startswith(
"Bearer ",
):
logger.debug(
"azure_bot auth: missing or malformed auth header",
)
return False
token = auth_header[7:] # Strip "Bearer "
await self._ensure_jwks_client()
if self._jwks_client is None:
logger.warning(
"azure_bot auth: JWKS client not available, "
"rejecting request",
)
return False
try:
signing_key = self._jwks_client.get_signing_key_from_jwt(
token,
)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=self._app_id,
options={
"verify_iss": False,
"verify_exp": True,
},
)
# Verify issuer is from Bot Framework
issuer = claims.get("iss", "")
if issuer not in self._VALID_ISSUERS:
# Accept tenant-specific issuers
if self._tenant_id and self._tenant_id in issuer:
pass
else:
logger.warning(
"azure_bot auth: unexpected issuer: %s",
issuer,
)
return False
return True
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as e:
logger.debug(
"azure_bot auth: token error: %s",
e,
)
return False
except Exception:
logger.exception(
"azure_bot auth: unexpected error",
)
return False