Share via

Architectural Review Request: Cross-Tenant Microsoft Teams Integration with Azure AI Foundry Agent via Custom Middleware

Supakrit (Tan) Aphonmaeklong 20 Reputation points
2026-03-29T09:39:10.04+00:00

Issue Type: Architecture & Design / Integration Issue Services: Azure AI Foundry, Azure Bot Service, Microsoft Entra ID

Environment Details:

  • Resource Tenant (hosts Azure AI & Bot): <ResourceTenant>
    • User/Home Tenant (hosts Microsoft Teams): <HomeTenant>
    • SDK Used: azure-ai-projects (v2.0.1+)

Context & Business Goal: We needed to deploy an internal Helpdesk AI Agent built in Azure AI Foundry (<ResourceTenant>) as a one-on-one personal chat bot to our end-users via Microsoft Teams (<HomeTenant>).

The Initial Challenge: The standard out-of-the-box Foundry Teams deployment (using the auto-generated .zip manifest and User-Assigned Managed Identity) hard-fails across tenant boundaries. Users in the foreign tenant are trapped in an endless OAuth "Sign-In" loop, resulting in a 500 Internal Server Error from the /protocols/activityprotocol endpoint.

Our Implemented Solution (The "Broker Pattern"): To bypass the direct OAuth link and successfully cross the tenant boundary for these 1-on-1 chats, we designed a custom stateful middleware proxy. The architecture consists of four layers:

  1. Identity & Access Management (Microsoft Entra ID)

<!-- end list -->

  • We created a custom App Registration in the resource tenant and configured it as Multi-Tenant.
    • We generated a Client Secret to securely authenticate our proxy to the Bot Framework and Foundry without relying on the locked-down Managed Identity.

<!-- end list -->

  1. The Traffic Gateway (Azure Bot Service)

<!-- end list -->

  • We provisioned a new Azure Bot resource configured as Single-Tenant (as the classic Multi-Tenant bot creation is deprecated) and tied it directly to our custom Multi-Tenant App Registration.
    • We configured the Bot's messaging endpoint to route all Teams webhook traffic to our custom Python middleware.

<!-- end list -->

  1. The Stateful Middleware (Python FastAPI Proxy)

<!-- end list -->

  • We built a custom Python microservice that acts as the translator between Teams and Foundry.
    • State Mapping: It extracts the Teams conversation.id and maps it to the Azure AI conversation.id to ensure the agent retains multi-turn memory for each user's 1-on-1 session.
    • AI Integration: It uses the azure-ai-projects (v2.0+) SDK via App-Only credentials to natively push messages and fetch AI responses using the responses.create protocol.
    • Cross-Tenant Delivery: It requests a Bot Framework token from the resource tenant endpoint and uses pure REST API (httpx) to POST the reply back to the smba.trafficmanager.net service URL. We strictly mirror the Teams replyToId, recipient, and conversation payload objects to successfully bypass Teams' strict 401 Unauthorized cross-tenant injection blocks.

<!-- end list -->

  1. The Client Interface (Microsoft Teams)

<!-- end list -->

  • We manually decoupled the auto-generated Foundry Teams manifest.json, mapping the id, botId, and webApplicationInfo strictly to our new Multi-Tenant App Registration, scoped it to "personal", and side-loaded it into the home tenant.

Our Ask for Architectural Review: The solution is currently working perfectly in proof-of-concept, maintaining context and securely crossing the tenant boundary using this hybrid Single-Tenant Bot / Multi-Tenant App Registration approach. We are requesting a high-level engineering review of the architecture:

  1. Is this "Broker Pattern" the officially recommended best practice for cross-tenant Foundry Agents, or are there native cross-tenant capabilities in Foundry that would eliminate the need for this custom middleware?
  2. From an Identity and Access Management perspective, are there any security enhancements you recommend for our custom API Bot Framework routing setup?
  3. Does Microsoft recommend migrating the raw REST API Bot Framework calls to the official botbuilder-core Python SDK for production workloads, or is our raw HTTP approach fully supported?

Please note: The code below is a simplified Proof-of-Concept strictly to illustrate the Teams-to-Foundry payload routing and OAuth token exchange. We are not requesting a code review of the application logic itself (e.g., we will be using external caches like Redis for state management in production, not the in-memory dictionary shown below). We are providing this solely to demonstrate the REST API calls used to cross the tenant boundary.

from fastapi import FastAPI, Request, Response, BackgroundTasks import httpx from azure.identity.aio import ClientSecretCredential from azure.ai.projects.aio import AIProjectClient

app = FastAPI()

--- 1. CREDENTIALS & CONFIGURATION ---

BOT_APP_ID = "<REDACTED_APP_ID>" BOT_APP_SECRET = "<REDACTED_APP_SECRET>" FOUNDRY_TENANT_ID = "<REDACTED_TENANT_ID>"

--- 2. AZURE AI FOUNDRY CONFIGURATION ---

AGENT_NAME = "<REDACTED_AGENT_NAME>"

credential = ClientSecretCredential( tenant_id=FOUNDRY_TENANT_ID, client_id=BOT_APP_ID, client_secret=BOT_APP_SECRET )

--- 3. IN-MEMORY STATE STORE (PoC ONLY) ---

session_memory = {}

async def process_and_reply(payload: dict, user_text: str): teams_conv_id = payload.get("conversation", {}).get("id") ai_response_text = ""

try:
    async with AIProjectClient(
        endpoint="<REDACTED_ENDPOINT>",
        subscription_id="<REDACTED_SUBSCRIPTON_ID>",
        resource_group_name="<REDACTED_RESOURCE_GROUP>",
        project_name="<REDACTED_PROJECT_NAME>",
        credential=credential
    ) as project_client:
        
        async with project_client.get_openai_client() as openai_client:
            
            if teams_conv_id in session_memory:
                azure_conv_id = session_memory[teams_conv_id]
                await openai_client.conversations.items.create(
                    conversation_id=azure_conv_id,
                    items=[{"type": "message", "role": "user", "content": user_text}]
                )
            else:
                conversation = await openai_client.conversations.create(
                    items=[{"type": "message", "role": "user", "content": user_text}]
                )
                azure_conv_id = conversation.id
                session_memory[teams_conv_id] = azure_conv_id
            
            response = await openai_client.responses.create(
                conversation=azure_conv_id,
                extra_body={"agent_reference": {"name": AGENT_NAME, "type": "agent_reference"}}
            )
            ai_response_text = response.output_text

except Exception as e:
    ai_response_text = "Sorry, I couldn't reach my internal knowledge base right now."

token_url = f"[https://login.microsoftonline.com/](https://login.microsoftonline.com/){FOUNDRY_TENANT_ID}/oauth2/v2.0/token"
token_data = {
    "grant_type": "client_credentials",
    "client_id": BOT_APP_ID,
    "client_secret": BOT_APP_SECRET,
    "scope": "[https://api.botframework.com/.default](https://api.botframework.com/.default)"
}

try:
    async with httpx.AsyncClient() as client:
        token_response = await client.post(token_url, data=token_data)
        token_response.raise_for_status()
        bot_token = token_response.json().get("access_token")

    service_url = payload.get("serviceUrl")
    reply_url = f"{service_url}v3/conversations/{teams_conv_id}/activities/{payload['id']}"

    reply_activity = {
        "type": "message",
        "text": ai_response_text,
        "replyToId": payload.get("id"),
        "from": payload.get("recipient"),           
        "recipient": payload.get("from"),
        "conversation": payload.get("conversation") 
    }

    headers = {
        "Authorization": f"Bearer {bot_token}",
        "Content-Type": "application/json"
    }

    async with httpx.AsyncClient(timeout=45.0) as client:
        post_response = await client.post(reply_url, json=reply_activity, headers=headers)
        post_response.raise_for_status()
        
except Exception as e:
    print(f"Failed to send message back to Teams: {str(e)}")

@app.post("/api/messages") async def handle_teams_messages(request: Request, background_tasks: BackgroundTasks): payload = await request.json() user_text = payload.get("text", "").strip()

if not user_text and payload.get("type") != "message":
    return Response(status_code=200)

background_tasks.add_task(process_and_reply, payload, user_text)
return Response(status_code=200)
Foundry Agent Service
Foundry Agent Service

A fully managed platform in Microsoft Foundry for hosting, scaling, and securing AI agents built with any supported framework or model

0 comments No comments

Answer accepted by question author
  1. SRILAKSHMI C 16,975 Reputation points Microsoft External Staff Moderator
    2026-04-02T09:00:05.2466667+00:00

    Hello Supakrit (Tan) Aphonmaeklong,

    Thanks for sharing the detailed broker-pattern implementation this is a well-thought-out and practical solution to a real platform limitation.

    1) Broker Pattern vs Native Cross-Tenant Support

    Today, the Teams integration generated from Azure AI Foundry (via the manifest package) is strictly single-tenant by design. It assumes that identity, bot registration, and user context all exist within the same tenant boundary. Because of this, cross-tenant personal bot scenarios like your 1:1 helpdesk assistant aren’t natively supported.

    That limitation is exactly why you encountered the OAuth loop and /activityprotocol failures when trying to use the out-of-the-box deployment across tenants.

    Given that constraint, your broker (middleware) pattern is effectively the de-facto architecture today for enabling cross-tenant communication. By introducing a controlled intermediary layer, you’ve:

    • Decoupled identity boundaries
    • Taken control of token acquisition and message routing
    • Preserved conversation state across systems
    • Bypassed the assumptions baked into the default deployment

    This isn’t a workaround in a negative sense it’s a forward-compatible integration pattern given current platform gaps.

    If your use case allowed channel-based (team scope) bots instead of personal chats, you could deploy the Teams app separately into each tenant and rely on the standard Bot Framework flow. However, that model does not satisfy personal 1:1 chat requirements, which is why your current approach is justified.

    No native cross-tenant Foundry - Teams personal chat capability exists today

    Your broker pattern is valid, practical, and aligned with how advanced customers are solving this

    2) Identity & Access Management

    Your current IAM setup is functional, but for production readiness, there are a few important upgrades worth implementing.

    Credential Strategy

    Move away from client secrets and adopt:

    • Certificate-based authentication where possible
    • Store credentials securely in Azure Key Vault with strict access policies

    This reduces exposure risk and improves rotation practices.

    Permission Scoping

    Right now, using broad .default scopes works, but it’s not ideal.

    Instead Scope your app registration to only required permissions

    • Bot Framework (e.g., send/reply capabilities)
    • Minimal Microsoft Graph permissions if used

    Avoid over-permissioning, especially in multi-tenant scenarios

    Conditional Access & Governance

    Since this is a cross-tenant integration, tightening access control is important:

    Apply Conditional Access policies in Microsoft Entra ID

    • Restrict by location/IP
    • Enforce stronger controls for sensitive operations

    Use Privileged Identity Management (PIM) for any elevated access scenarios

    Regularly rotate credentials and audit usage

    Token Validation

    Your middleware should explicitly validate incoming Bot Framework requests:

    • Validate JWT signature, issuer, and audience
    • Ensure requests genuinely originate from trusted Bot Framework endpoints

    This prevents spoofing and unauthorized message injection—especially important since your API is now exposed as a cross-tenant bridge.

    3) Raw REST vs BotBuilder SDK

    Your current implementation using raw HTTP calls to the Bot Framework APIs is fully supported and valid. There’s nothing inherently wrong with continuing this approach.

    However, from a production engineering perspective, there are trade-offs.

    Raw REST

    Full control over request/response handling

    Lightweight and flexible

    But requires you to manually handle:

    • Token caching and refresh
    • Retry logic
    • Activity schema validation
    • Error handling consistency

    SDK Approach

    Using the official Python SDK botbuilder-core gives you:

    • Built-in authentication handling
    • Automatic token management
    • Retry and resiliency mechanisms
    • Standardized activity processing
    • Easier telemetry integration

    Recommendation

    • For PoC - your REST approach is perfectly fine
    • For production - strongly recommend moving to the SDK to reduce operational overhead and improve maintainability

    Please refer this

    • Baseline Microsoft Foundry chat reference architecture – https://dotnet.territoriali.olinfo.it/azure/architecture/ai-ml/architecture/baseline-microsoft-foundry-chat

    • Basic Microsoft Foundry chat reference architecture – https://dotnet.territoriali.olinfo.it/azure/architecture/ai-ml/architecture/basic-microsoft-foundry-chat

    • Baseline Foundry chat in a landing zone – https://dotnet.territoriali.olinfo.it/azure/architecture/ai-ml/architecture/baseline-microsoft-foundry-landing-zone

    I Hope this helps. Do let me know if you have any further queries.


    If this answers your query, please do click Accept Answer and Yes for was this answer helpful.

    Thank you!


0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.