Commit Graph

1 Commits

  • Python: add agent-framework-hosting-entra identity-link helpers (#5644)
    * feat(hosting-entra): add Entra (Azure AD) identity-linking channel
    
    New ``agent-framework-hosting-entra`` package implementing a Microsoft
    Entra OAuth-based identity-linking channel for the Hosting framework.
    Mounts a small set of routes (``/entra/login``, ``/entra/callback``,
    ``/entra/whoami``) that walk a user through an Entra/Azure AD
    authorization-code flow and stick the resulting verified identity
    (``oid`` / ``email`` / ``tid``) onto the host's identity table so
    later requests on any other channel (Responses, Telegram, …) can be
    linked to the same user.
    
    Surface (re-exported from ``agent_framework_hosting_entra``):
    
    - ``EntraChannel`` -- concrete ``Channel`` implementation. Owns the
      three Starlette routes, signs/verifies short-lived ``state`` tokens
      to bind the round-trip to the originating channel, exchanges the
      authorization code for an ID token via MSAL, and writes the
      verified identity into the host's identity store via the standard
      ``ChannelIdentity`` plumbing so cross-channel push (e.g. send a
      Telegram message to the user who completed the link from
      Responses) works without the channels having to coordinate
      directly.
    - 14 unit tests covering route wiring, ``state`` issue / verify,
      callback exchange happy + failure paths, and identity-store write.
    
    Registers the package in ``python/pyproject.toml``
    ``[tool.uv.sources]`` and adds the matching pyright
    ``executionEnvironments`` entry. Stacks on PR-2 (Hosting core);
    independent of PR-3 / PR-4 / PR-6.
    
    The cross-channel sample (``local_identity_link/``) that demonstrates
    this end-to-end alongside Responses + Telegram lands in PR-8 (samples).
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * fix(hosting-entra): close IDOR + reflected-XSS + open-redirect on the OAuth flow
    
    Three SECURITY-CRITICAL fixes flagged in round-2 review.
    
    1. IDOR on /auth/start (3198518308). Without authentication the
       endpoint accepted (channel, channel_id) from the query string and
       bound *whoever signed in* to that pair. An attacker could bind
       their own Entra oid to a victim's per-channel id (e.g.
       `telegram:<victim_chat_id>`), redirecting all of the victim's
       future inbound traffic to the attacker's isolation key.
    
       Fix: introduce link_token_secret + mint_start_url(channel, id, ...).
       When set, /auth/start requires `exp` + `sig` (HMAC-SHA256 over
       `channel|channel_id|expires_at`) before issuing the redirect.
       Channels that hand out start URLs (a Telegram /link command after
       verifying the inbound webhook signature) call mint_start_url so
       the token proves the (channel, id) pair was authorised by the
       channel that owns the surface. Unsigned mode is opt-in and logs a
       loud WARNING at startup *and* on every accepted request.
    
    2. Reflected XSS on /auth/callback (3198520256, 3198527896). `error`,
       `error_description`, channel_key (from the unauthenticated /start
       query), and `upn` (from a Graph response) flowed straight into the
       text/html response body unescaped. With the IDOR above, an
       attacker could stash `<script>` payloads in `channel` or `id` and
       serve them from the auth host's origin (full XSS on the auth
       surface — cookies/storage of anything else mounted there).
    
       Fix: html.escape() every value before HTML output.
    
    3. Open redirect on `return_to` (3198524746). Accepted any URL.
    
       Fix: `_validate_return_to` allows only relative paths starting
       with `/` (and not `//`) or absolute URLs whose host equals the
       configured `public_base_url` host. Validated at /start mint time
       AND defensively re-validated at /callback before redirect.
    
    12 new tests cover signed-token rejection (missing/forged/expired),
    mint helper requirements, startup warning visibility, XSS escaping
    on both error and success paths, and the open-redirect allowlist
    (external rejected, relative accepted, same-origin accepted,
    protocol-relative `//evil.example/` rejected).
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    * test(hosting): drop redundant @pytest.mark.asyncio decorators
    
    asyncio_mode = "auto" is configured in pyproject.toml across the
    hosting packages, so individual @pytest.mark.asyncio decorators are
    unnecessary.
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
    
    ---------
    
    Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>