
{"id":90,"date":"2026-09-16T15:55:05","date_gmt":"2026-09-16T15:55:05","guid":{"rendered":"https:\/\/roboticsmaestro.com\/ai\/?p=90"},"modified":"2026-09-16T19:41:10","modified_gmt":"2026-09-16T19:41:10","slug":"setting-up-the-pipeline-openwebui-mcp-function-app-with-oauth-ai-search-azure-blob-storage-using-private-endpoint-optional-foundry-agent","status":"publish","type":"post","link":"https:\/\/roboticsmaestro.com\/ai\/blog\/2026\/09\/16\/setting-up-the-pipeline-openwebui-mcp-function-app-with-oauth-ai-search-azure-blob-storage-using-private-endpoint-optional-foundry-agent\/","title":{"rendered":"Setting up the pipeline &#8211; openwebui + mcp function app with Oauth + ai-search + azure blob storage (using private endpoint) + optional foundry agent"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Visualizing your architecture<\/strong> with a <strong>sequence diagram<\/strong> and <strong>class diagram<\/strong> so we\u2019re clear on the moving parts.<\/li>\n\n\n\n<li><strong>Targeted OAuth troubleshooting<\/strong> for your <strong>Azure Function App \u2192 MCP \u2192 Azure AI Search<\/strong> setup over <strong>private endpoints<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Sequence Diagram<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s a <strong>Mermaid<\/strong> sequence diagram showing the flow:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Mermaid<br> <code>sequenceDiagram<br>    participant User as Website (Browser)<br>    participant MCP as MCP Service (Private Endpoint)<br>    participant Func as Azure Function App<br>    participant OAuth as Azure Entra ID (OAuth)<br>    participant Search as Azure AI Search (Private Endpoint)<br>    participant Blob as Azure Blob Storage (Private Endpoint)<br><br>    User-&gt;&gt;MCP: HTTPS Request (Authenticated via OAuth)<br>    MCP-&gt;&gt;OAuth: OAuth Token Request (Client Credentials \/ Auth Code)<br>    OAuth--&gt;&gt;MCP: Access Token (Bearer)<br>    MCP-&gt;&gt;Func: Forward Request with Token<br>    Func-&gt;&gt;Search: REST API Call (Index Query\/Run) with Token<br>    Search-&gt;&gt;Blob: Read Files from Blob Storage<br>    Blob--&gt;&gt;Search: File Content<br>    Search--&gt;&gt;Func: Search Results \/ Indexing Status<br>    Func--&gt;&gt;MCP: Response<br>    MCP--&gt;&gt;User: Final Response<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Class Diagram<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">MermaidCopy code<code>classDiagram<br>    class Website {<br>        +sendRequest()<br>        +displayResults()<br>    }<br><br>    class MCPService {<br>        +authenticateWithOAuth()<br>        +forwardRequest()<br>        +enforcePrivateEndpoint()<br>    }<br><br>    class AzureFunctionApp {<br>        +triggerIndexer()<br>        +callSearchAPI()<br>        +handleOAuthToken()<br>    }<br><br>    class AzureAIsearch {<br>        +runIndexer()<br>        +queryIndex()<br>        +connectToBlob()<br>    }<br><br>    class BlobStorage {<br>        +listFiles()<br>        +readFile()<br>    }<br><br>    class AzureEntraID {<br>        +issueToken()<br>        +validateToken()<br>    }<br><br>    Website --&gt; MCPService<br>    MCPService --&gt; AzureFunctionApp<br>    AzureFunctionApp --&gt; AzureAIsearch<br>    AzureAIsearch --&gt; BlobStorage<br>    MCPService --&gt; AzureEntraID<br>    AzureFunctionApp --&gt; AzureEntraID<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">walk you through <strong>every single step<\/strong> to set up OAuth correctly for your <strong>Azure Function App \u2192 MCP \u2192 Azure AI Search (private endpoints) \u2192 Blob Storage<\/strong> architecture.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will be <strong>end-to-end<\/strong>, so you can follow it without missing anything.<br>We\u2019ll cover <strong>App Registration<\/strong>, <strong>Permissions<\/strong>, <strong>Network<\/strong>, <strong>Function App config<\/strong>, and <strong>Testing<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step-by-Step OAuth Setup<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>1. Plan Your Authentication Flow<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You have two main options:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Managed Identity<\/strong> (recommended for Function Apps in Azure) \u2014 no secrets, simpler, more secure.<\/li>\n\n\n\n<li><strong>Client Credentials (OAuth 2.0)<\/strong> \u2014 requires Client ID + Secret.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If you can, <strong>use Managed Identity<\/strong> \u2014 it avoids most OAuth pitfalls.<br>If you must use Client Credentials, follow all steps below.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>2. Create \/ Configure Azure Entra ID App Registration<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <strong>Azure Portal \u2192 Azure Entra ID \u2192 App registrations \u2192 New registration<\/strong>.<\/li>\n\n\n\n<li><strong>Name<\/strong>: <code>FunctionApp-MCP-Search-Blob<\/code><\/li>\n\n\n\n<li><strong>Supported account types<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Usually: <em>Accounts in this organizational directory only<\/em>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Redirect URI<\/strong>:\n<ul class=\"wp-block-list\">\n<li>For <code>client_credentials<\/code> flow, you can skip this.<\/li>\n\n\n\n<li>For <code>authorization_code<\/code> flow (browser-based), set it to your MCP or Function App callback URL exactly.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Click <strong>Register<\/strong>.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>3. Add API Permissions<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In the App Registration \u2192 <strong>API permissions<\/strong> \u2192 <strong>Add a permission<\/strong>:\n<ul class=\"wp-block-list\">\n<li><strong>Microsoft APIs \u2192 Azure Cognitive Search<\/strong> \u2192 <code>user_impersonation<\/code> (or use <code>.default<\/code> scope later).<\/li>\n\n\n\n<li><strong>Microsoft APIs \u2192 Azure Storage<\/strong> \u2192 <code>user_impersonation<\/code>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Click <strong>Add permissions<\/strong>.<\/li>\n\n\n\n<li>Click <strong>Grant admin consent<\/strong> for your tenant.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>4. Create Client Secret (if not using Managed Identity)<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In App Registration \u2192 <strong>Certificates &amp; secrets<\/strong> \u2192 <strong>New client secret<\/strong>.<\/li>\n\n\n\n<li><strong>Description<\/strong>: <code>FunctionAppSecret<\/code><\/li>\n\n\n\n<li><strong>Expires<\/strong>: Choose 6\u201312 months (rotate before expiry).<\/li>\n\n\n\n<li>Copy the <strong>Value<\/strong> immediately \u2014 you won\u2019t see it again.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>5. Assign Roles to the Identity<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Whether using Managed Identity or App Registration:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Azure AI Search<\/strong>: Assign <strong>Search Index Data Contributor<\/strong> role.<\/li>\n\n\n\n<li><strong>Blob Storage<\/strong>: Assign <strong>Storage Blob Data Reader<\/strong> role.<\/li>\n\n\n\n<li>Scope: Assign at the <strong>resource level<\/strong> (Search service and Storage account).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>6. Configure Network for OAuth<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">OAuth endpoints are <strong>public<\/strong> \u2014 even if Search and Blob are private.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure outbound HTTPS (port 443) from Function App to:\n<ul class=\"wp-block-list\">\n<li><code>login.microsoftonline.com<\/code><\/li>\n\n\n\n<li><code>login.microsoft.com<\/code><\/li>\n\n\n\n<li><code>sts.windows.net<\/code><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>If using Private DNS Zones:\n<ul class=\"wp-block-list\">\n<li>Do <strong>not<\/strong> override <code>microsoftonline.com<\/code>.<\/li>\n\n\n\n<li>Ensure <code>privatelink.search.windows.net<\/code> and <code>privatelink.blob.core.windows.net<\/code> resolve to private IPs.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>7. Configure Azure Function App Settings<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In <strong>Function App \u2192 Configuration \u2192 Application settings<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If using Client Credentials:Copy code<code>TENANT_ID=&lt;your-tenant-id&gt; CLIENT_ID=&lt;your-app-registration-client-id&gt; CLIENT_SECRET=&lt;your-client-secret&gt; SEARCH_SERVICE=&lt;your-search-service-name&gt;<\/code><\/li>\n\n\n\n<li>If using Managed Identity:Copy code<code>SEARCH_SERVICE=&lt;your-search-service-name&gt;<\/code><\/li>\n\n\n\n<li>Ensure <strong>VNet integration<\/strong> is enabled if you need private endpoint access.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>8. Implement OAuth in Code<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Client Credentials Example:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from azure.identity import ClientSecretCredential\nimport requests, os\n\ntenant_id = os.environ[\"TENANT_ID\"]\nclient_id = os.environ[\"CLIENT_ID\"]\nclient_secret = os.environ[\"CLIENT_SECRET\"]\nsearch_service = os.environ[\"SEARCH_SERVICE\"]\n\ncred = ClientSecretCredential(tenant_id, client_id, client_secret)\ntoken = cred.get_token(\"https:\/\/search.azure.com\/.default\")\n\nurl = f\"https:\/\/{search_service}.search.windows.net\/indexes?api-version=2023-11-01\"\nheaders = {\"Authorization\": f\"Bearer {token.token}\"}\nr = requests.get(url, headers=headers)\nprint(r.status_code, r.text)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Managed Identity Example:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from azure.identity import ManagedIdentityCredential\nimport requests, os\n\nsearch_service = os.environ[\"SEARCH_SERVICE\"]\n\ncred = ManagedIdentityCredential()\ntoken = cred.get_token(\"https:\/\/search.azure.com\/.default\")\n\nurl = f\"https:\/\/{search_service}.search.windows.net\/indexes?api-version=2023-11-01\"\nheaders = {\"Authorization\": f\"Bearer {token.token}\"}\nr = requests.get<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong> and finish the <strong>OAuth setup guide<\/strong> with the <strong>final implementation, validation, and troubleshooting steps<\/strong> so you can be 100% confident it\u2019s working in your<br><strong>Azure Function App \u2192 MCP \u2192 Azure AI Search (private endpoints) \u2192 Blob Storage<\/strong> flow.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>9. Validate OAuth Token<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before wiring it into MCP or your production code, test the token manually.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>From Function App Kudu Console or Cloud Shell:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X POST \\\n  -H \"Content-Type: application\/x-www-form-urlencoded\" \\\n  -d \"client_id=$CLIENT_ID\" \\\n  -d \"client_secret=$CLIENT_SECRET\" \\\n  -d \"scope=https:\/\/search.azure.com\/.default\" \\\n  -d \"grant_type=client_credentials\" \\\n  https:\/\/login.microsoftonline.com\/$TENANT_ID\/oauth2\/v2.0\/token\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>Expected:<\/strong> JSON with <code>access_token<\/code> and <code>expires_in<\/code>.<br>&#x274c; <strong>If fails:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Check <code>TENANT_ID<\/code>, <code>CLIENT_ID<\/code>, <code>CLIENT_SECRET<\/code> values.<\/li>\n\n\n\n<li>Ensure outbound HTTPS to <code>login.microsoftonline.com<\/code> is allowed.<\/li>\n\n\n\n<li>Verify API permissions and <strong>Admin Consent<\/strong> in Azure Entra ID.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>10. Test Azure AI Search API with Token<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have the token:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X GET \\\n  -H \"Authorization: Bearer &lt;access_token&gt;\" \\\n  \"https:\/\/&lt;search-service&gt;.search.windows.net\/indexes?api-version=2023-11-01\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>Expected:<\/strong> JSON listing indexes.<br>&#x274c; <strong>If fails with 403\/401:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Token audience mismatch \u2014 check that you requested <code>https:\/\/search.azure.com\/.default<\/code>.<\/li>\n\n\n\n<li>Missing <strong>Search Index Data Contributor<\/strong> role for the identity.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>11. Test Blob Storage Access (If Indexer Reads from Blob)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If your Search indexer pulls from Blob Storage:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>curl -X GET \\\n  -H \"Authorization: Bearer &lt;access_token&gt;\" \\\n  \"https:\/\/&lt;storage-account&gt;.blob.core.windows.net\/&lt;container&gt;?restype=container&amp;comp=list\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>Expected:<\/strong> XML listing blobs.<br>&#x274c; <strong>If fails:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Missing <strong>Storage Blob Data Reader<\/strong> role.<\/li>\n\n\n\n<li>Private endpoint DNS misconfiguration for Blob.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>12. Integrate with MCP<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once OAuth works in isolation:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>MCP should <strong>forward the token<\/strong> from the Function App or request its own token using the same method.<\/li>\n\n\n\n<li>If MCP is in a private subnet, ensure it also has outbound HTTPS to <code>login.microsoftonline.com<\/code>.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>13. Common Pitfalls Checklist<\/strong><\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Issue<\/th><th class=\"has-text-align-left\" data-align=\"left\">Symptom<\/th><th class=\"has-text-align-left\" data-align=\"left\">Fix<\/th><\/tr><\/thead><tbody><tr><td><strong>Private DNS hijacking<\/strong><\/td><td>DNS to <code>login.microsoftonline.com<\/code> resolves to private IP<\/td><td>Remove private zone for <code>microsoftonline.com<\/code><\/td><\/tr><tr><td><strong>Outbound HTTPS blocked<\/strong><\/td><td>Curl to OAuth endpoint fails<\/td><td>Update NSG\/firewall to allow port 443<\/td><\/tr><tr><td><strong>Wrong scope<\/strong><\/td><td>Token works but API returns 401<\/td><td>Use <code>https:\/\/search.azure.com\/.default<\/code><\/td><\/tr><tr><td><strong>Missing admin consent<\/strong><\/td><td>Token request fails with <code>AADSTS65001<\/code><\/td><td>Grant admin consent in Entra ID<\/td><\/tr><tr><td><strong>Role missing<\/strong><\/td><td>Token works but API returns 403<\/td><td>Assign Search Index Data Contributor \/ Blob Data Reader<\/td><\/tr><tr><td><strong>Expired secret<\/strong><\/td><td>Token request fails<\/td><td>Rotate client secret<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>14. Final Verification Flow<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run this <strong>inside your Function App<\/strong> to confirm all hops:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">PythonCopy code<code>from azure.identity import ClientSecretCredential, ManagedIdentityCredential\nimport os, requests\n\nsearch_service = os.environ[\"SEARCH_SERVICE\"]\nscope = \"https:\/\/search.azure.com\/.default\"\n\nif os.environ.get(\"CLIENT_SECRET\"):\n    cred = ClientSecretCredential(\n        os.environ[\"TENANT_ID\"],\n        os.environ[\"CLIENT_ID\"],\n        os.environ[\"CLIENT_SECRET\"]\n    )\nelse:\n    cred = ManagedIdentityCredential()\n\ntoken = cred.get_token(scope)\nprint(\"&#x2705; Token acquired\")\n\nurl = f\"https:\/\/{search_service}.search.windows.net\/indexes?api-version=2023-11-01\"\nheaders = {\"Authorization\": f\"Bearer {token.token}\"}\nr = requests.get(url, headers=headers)\nprint(f\"Search API status: {r.status_code}\")\nprint(r.text[:200] + \"...\")\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>If this works<\/strong> \u2014 OAuth is fully configured and your Function App can securely talk to Azure AI Search over private endpoints.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">the <strong>OAuth handshake diagram<\/strong> for your<br><strong>Azure Function App \u2192 MCP \u2192 Azure AI Search (private endpoints) \u2192 Blob Storage<\/strong> setup.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will clearly show your <strong>private endpoint traffic<\/strong> vs <strong>public OAuth traffic<\/strong>, so your <strong>network\/security team<\/strong> can validate the flow.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Mermaid OAuth Handshake Diagram<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">MermaidCopy code<code>sequenceDiagram\n    participant User as Website (Browser)\n    participant MCP as MCP Service (Private Endpoint)\n    participant Func as Azure Function App (VNet Integrated)\n    participant OAuth as Azure Entra ID (Public Endpoint)\n    participant Search as Azure AI Search (Private Endpoint)\n    participant Blob as Azure Blob Storage (Private Endpoint)\n\n    User-&gt;&gt;MCP: HTTPS Request (User Action)\n    MCP-&gt;&gt;Func: Forward API Request\n    Note over Func,OAuth: Step 1: Function App requests OAuth token<br>via Client Credentials or Managed Identity\n    Func-&gt;&gt;OAuth: POST \/{tenant}\/oauth2\/v2.0\/token (Public Internet)\n    OAuth--&gt;&gt;Func: Access Token (Bearer)\n    Note over Func,Search: Step 2: Function App calls Azure AI Search<br>over Private Endpoint with token\n    Func-&gt;&gt;Search: GET \/indexes?api-version=2023-11-01 (Bearer token)\n    Search-&gt;&gt;Blob: Read files from Blob Storage (Private Endpoint)\n    Blob--&gt;&gt;Search: File Content\n    Search--&gt;&gt;Func: Search Results \/ Indexing Status\n    Func--&gt;&gt;MCP: Response\n    MCP--&gt;&gt;User: Final Response\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Key Points for Security Review<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>OAuth endpoints (<code>login.microsoftonline.com<\/code>) remain public<\/strong> \u2014 must allow outbound HTTPS from Function App and MCP.<\/li>\n\n\n\n<li><strong>Azure AI Search &amp; Blob Storage traffic stays private<\/strong> \u2014 resolved via Private DNS Zones to private IPs.<\/li>\n\n\n\n<li><strong>Token never leaves your Azure environment<\/strong> \u2014 MCP and Function App handle it internally.<\/li>\n\n\n\n<li><strong>Role-based access control<\/strong> ensures token is scoped only to required services:\n<ul class=\"wp-block-list\">\n<li><code>Search Index Data Contributor<\/code> for Search<\/li>\n\n\n\n<li><code>Storage Blob Data Reader<\/code> for Blob<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>No secrets in code<\/strong> if using Managed Identity \u2014 reduces risk.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Traffic Flow Summary<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Public Path:<\/strong> Function App \u2192 Azure Entra ID (OAuth token request)<\/li>\n\n\n\n<li><strong>Private Path:<\/strong> Function App \u2192 Azure AI Search \u2192 Blob Storage<\/li>\n\n\n\n<li><strong>Token Scope:<\/strong> <code>https:\/\/search.azure.com\/.default<\/code> (and optionally <code>https:\/\/storage.azure.com\/.default<\/code>)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>. OAuth Connectivity Failure Troubleshooting (Private Endpoints)<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When <strong>OAuth<\/strong> fails in a <strong>private endpoint<\/strong> setup, the most common issues are <strong>DNS resolution<\/strong>, <strong>firewall rules<\/strong>, and <strong>redirect URI mismatches<\/strong>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Checklist<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>A. Network &amp; DNS<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure the Function App\u2019s <strong>VNet integration<\/strong> allows outbound traffic to <code>login.microsoftonline.com<\/code> and <code>login.microsoft.com<\/code> (OAuth endpoints are <em>public<\/em>, even if your services are private).<\/li>\n\n\n\n<li>If you\u2019re using <strong>Private DNS Zones<\/strong>, make sure you haven\u2019t overridden public Microsoft identity endpoints.<\/li>\n\n\n\n<li>Test from the Function App console:BashCopy code<code>curl -v https:\/\/login.microsoftonline.com\/&lt;tenant-id&gt;\/v2.0\/.well-known\/openid-configuration<\/code><\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>B. Redirect URI<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>In <strong>Azure Entra ID App Registration<\/strong>, confirm:\n<ul class=\"wp-block-list\">\n<li>Redirect URI matches exactly what MCP or Function App uses (case-sensitive, no trailing slash mismatch).<\/li>\n\n\n\n<li>If using <code>authorization_code<\/code> flow, the URI must be reachable from the browser.<\/li>\n\n\n\n<li>For <code>client_credentials<\/code> flow, redirect URI is not used \u2014 but scopes must be correct.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>C. Scopes &amp; Permissions<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>For Azure AI Search: <code>https:\/\/search.azure.com\/.default<\/code><\/li>\n\n\n\n<li>For Blob Storage: <code>https:\/\/storage.azure.com\/.default<\/code><\/li>\n\n\n\n<li>Grant <strong>Admin Consent<\/strong> in Azure Entra ID.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>D. Private Endpoint Gotchas<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Azure AI Search and Blob Storage can be private, but <strong>OAuth endpoints cannot<\/strong> \u2014 they must remain public.<\/li>\n\n\n\n<li>If your Function App is in a locked-down VNet, you must allow outbound to Microsoft identity IP ranges.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>E. Token Validation<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>From the Function App, try:PythonCopy code<code>import requests, os token_url = f\"https:\/\/login.microsoftonline.com\/{os.environ['TENANT_ID']}\/oauth2\/v2.0\/token\" data = { \"client_id\": os.environ[\"CLIENT_ID\"], \"client_secret\": os.environ[\"CLIENT_SECRET\"], \"scope\": \"https:\/\/search.azure.com\/.default\", \"grant_type\": \"client_credentials\" } r = requests.post(token_url, data=data) print(r.status_code, r.text)<\/code>\n<ul class=\"wp-block-list\">\n<li>If this fails, it\u2019s <strong>OAuth config\/network<\/strong>.<\/li>\n\n\n\n<li>If it works, the issue is <strong>MCP \u2192 Function App token forwarding<\/strong>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>Key Insight:<\/strong><br>In private endpoint setups, **OAuth always requires public identity endpoints<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\">&#8211;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;if your <strong>direct token request<\/strong> from the Azure Function App still failed, that means we\u2019re not just dealing with an MCP forwarding issue \u2014 the problem is likely in <strong>network reachability, identity configuration, or TLS handshake<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s go deeper and <strong>systematically isolate<\/strong> where the OAuth flow is breaking.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 1 \u2014 Confirm Network Path to Azure Entra ID<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even with private endpoints for Blob and Search, <strong>OAuth endpoints are always public<\/strong>.<br>If your Function App is VNet-integrated, outbound traffic to <code>login.microsoftonline.com<\/code> might be blocked.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Test from Function App Console (Kudu \/ SSH):<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>nslookup login.microsoftonline.com\ncurl -v https:\/\/login.microsoftonline.com\/common\/discovery\/v2.0\/keys\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>If DNS fails<\/strong> \u2192 Check Private DNS Zones; remove or override any private zone for <code>microsoftonline.com<\/code>.<\/li>\n\n\n\n<li><strong>If curl fails<\/strong> \u2192 Check NSG, firewall, or route table blocking outbound HTTPS.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 2 \u2014 Check TLS &amp; Cipher Compatibility<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Some locked-down VNets or custom outbound proxies strip TLS handshakes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Test TLS handshake:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">BashCopy code<code>openssl s_client -connect login.microsoftonline.com:443 -servername login.microsoftonline.com\n<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If handshake fails, your outbound path is blocked or intercepting SSL.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 3 \u2014 Validate App Registration<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In <strong>Azure Entra ID \u2192 App Registrations<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Client ID<\/strong> and <strong>Client Secret<\/strong> match exactly.<\/li>\n\n\n\n<li><strong>Allowed grant type<\/strong> matches your flow (<code>client_credentials<\/code> for Function App \u2192 MCP \u2192 Search).<\/li>\n\n\n\n<li><strong>API permissions<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>https:\/\/search.azure.com\/.default<\/code><\/li>\n\n\n\n<li><code>https:\/\/storage.azure.com\/.default<\/code> (if Blob access is needed)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Admin consent<\/strong> granted.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 4 \u2014 Try Token Request from Outside the VNet<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Run the same Python <code>requests.post()<\/code> from:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Your local machine<\/li>\n\n\n\n<li>Azure Cloud Shell<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">If it works outside but fails inside \u2192 <strong>network isolation issue<\/strong> in your Function App\u2019s VNet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 5 \u2014 Use Managed Identity Instead of Client Secret<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If possible, skip OAuth client credentials entirely:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Enable <strong>System-Assigned Managed Identity<\/strong> on the Function App.<\/li>\n\n\n\n<li>Assign:\n<ul class=\"wp-block-list\">\n<li><strong>Search Index Data Contributor<\/strong> on Azure AI Search<\/li>\n\n\n\n<li><strong>Storage Blob Data Reader<\/strong> on Blob Storage<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>In code:PythonCopy code<code>from azure.identity import ManagedIdentityCredential cred = ManagedIdentityCredential() token = cred.get_token(\"https:\/\/search.azure.com\/.default\") print(token.token)<\/code>\n<ul class=\"wp-block-list\">\n<li>If this works, you\u2019ve bypassed the need for outbound OAuth calls to public endpoints with secrets.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Step 6 \u2014 Check MCP\u2019s Role in the Flow<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If MCP is still part of the authentication chain:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ensure MCP itself can reach <code>login.microsoftonline.com<\/code>.<\/li>\n\n\n\n<li>If MCP is in a private subnet, it may need a NAT Gateway or outbound rule.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">&#x1f4a1; <strong>Pro Tip:<\/strong><br>In <strong>100% private endpoint architectures<\/strong>, the most common OAuth failure is <strong>DNS hijacking<\/strong> \u2014 a Private DNS Zone accidentally intercepts <code>login.microsoftonline.com<\/code> and points it to a non-public IP, breaking the token request.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">let\u2019s map out the <strong>failure-path sequence diagram<\/strong> so you can see exactly where OAuth can break in your <strong>Azure Function App \u2192 MCP \u2192 Azure AI Search (private endpoints)<\/strong> setup.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will help you test <strong>each hop in isolation<\/strong> and pinpoint the failure.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Failure Path Sequence Diagram<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">MermaidCopy code<code>sequenceDiagram\n    participant User as Website (Browser)\n    participant MCP as MCP Service (Private Endpoint)\n    participant Func as Azure Function App (VNet Integrated)\n    participant OAuth as Azure Entra ID (Public Endpoint)\n    participant Search as Azure AI Search (Private Endpoint)\n    participant Blob as Azure Blob Storage (Private Endpoint)\n\n    User-&gt;&gt;MCP: HTTPS Request\n    MCP-&gt;&gt;Func: Forward Request\n    Func-&gt;&gt;OAuth: Request Access Token (client_credentials)\n    Note over Func,OAuth: &#x274c; Possible Failures:<br>1. DNS resolution blocked<br>2. Outbound HTTPS blocked<br>3. TLS handshake failure<br>4. Invalid client credentials<br>5. Wrong scope or tenant ID\n    OAuth--&gt;&gt;Func: Access Token (Bearer)\n    Func-&gt;&gt;Search: REST API Call with Token\n    Note over Func,Search: &#x274c; Possible Failures:<br>6. Token audience mismatch<br>7. Role assignment missing<br>8. Private endpoint DNS misconfig\n    Search-&gt;&gt;Blob: Read Files\n    Note over Search,Blob: &#x274c; Possible Failures:<br>9. Blob role missing<br>10. Private endpoint DNS misconfig\n    Blob--&gt;&gt;Search: File Content\n    Search--&gt;&gt;Func: Search Results\n    Func--&gt;&gt;MCP: Response\n    MCP--&gt;&gt;User: Final Response\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How to Test Each Hop<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Hop 1: Function App \u2192 Azure Entra ID<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Goal:<\/strong> Ensure token request works from inside the VNet.<\/li>\n\n\n\n<li><strong>Test:<\/strong>BashCopy code<code>curl -v https:\/\/login.microsoftonline.com\/&lt;tenant-id&gt;\/v2.0\/.well-known\/openid-configuration<\/code>\n<ul class=\"wp-block-list\">\n<li>If fails \u2192 DNS or outbound HTTPS issue.<\/li>\n\n\n\n<li>If succeeds \u2192 Try Python token request with <code>client_credentials<\/code>.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Hop 2: Token Audience Validation<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Goal:<\/strong> Ensure token is valid for Azure AI Search.<\/li>\n\n\n\n<li><strong>Test:<\/strong>PythonCopy code<code>from azure.identity import ClientSecretCredential cred = ClientSecretCredential(\"&lt;tenant-id&gt;\", \"&lt;client-id&gt;\", \"&lt;client-secret&gt;\") token = cred.get_token(\"https:\/\/search.azure.com\/.default\") print(token.token)<\/code>\n<ul class=\"wp-block-list\">\n<li>If fails \u2192 Wrong scope, missing admin consent, or invalid secret.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Hop 3: Function App \u2192 Azure AI Search (Private Endpoint)<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Goal:<\/strong> Ensure Function App can reach Search endpoint.<\/li>\n\n\n\n<li><strong>Test:<\/strong>BashCopy code<code>nslookup &lt;search-service-name&gt;.search.windows.net curl -v https:\/\/&lt;search-service-name&gt;.search.windows.net\/indexes?api-version=2023-11-01 -H \"Authorization: Bearer &lt;token&gt;\"<\/code>\n<ul class=\"wp-block-list\">\n<li>If DNS resolves to private IP but fails to connect \u2192 NSG or firewall issue.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Hop 4: Azure AI Search \u2192 Blob Storage (Private Endpoint)<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Goal:<\/strong> Ensure Search indexer can read from Blob.<\/li>\n\n\n\n<li><strong>Test:<\/strong> In Azure Portal, run the indexer manually and check logs.\n<ul class=\"wp-block-list\">\n<li>If fails \u2192 Role assignment or private DNS misconfig for Blob.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Common OAuth Failure Root Causes in This Setup<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Private DNS Zone hijacking<\/strong> <code>login.microsoftonline.com<\/code> \u2192 breaks token requests.<\/li>\n\n\n\n<li><strong>Outbound HTTPS blocked<\/strong> from Function App to public Microsoft identity endpoints.<\/li>\n\n\n\n<li><strong>Wrong OAuth scope<\/strong> (<code>https:\/\/search.azure.com\/.default<\/code> vs <code>https:\/\/storage.azure.com\/.default<\/code>).<\/li>\n\n\n\n<li><strong>Missing admin consent<\/strong> for API permissions.<\/li>\n\n\n\n<li><strong>Token audience mismatch<\/strong> \u2014 token issued for wrong resource.<\/li>\n\n\n\n<li><strong>Expired or incorrect client secret<\/strong> in Function App configuration.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">If you want, I can now give you a <strong>step-by-step \u201cisolation test script\u201d<\/strong> that you can run <strong>inside your Function App<\/strong> to automatically check all 4 hops and print exactly where it fails.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That way, you\u2019ll know in one run whether it\u2019s <strong>network, OAuth config, or private endpoint DNS<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">let\u2019s build you a <strong>self-contained diagnostic script<\/strong> that you can run <strong>inside your Azure Function App<\/strong> (via Kudu console or as a temporary function) to test <strong>all 4 hops<\/strong> in your<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Azure Function App \u2192 MCP \u2192 Azure AI Search (private endpoints) \u2192 Blob Storage<\/strong> OAuth flow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This will pinpoint exactly where the failure is happening.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Python Diagnostic Script<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">Python<br><br><code>import os<br>import socket<br>import requests<br>from azure.identity import ClientSecretCredential, ManagedIdentityCredential<br><br>TENANT_ID = os.environ.get(\"TENANT_ID\")<br>CLIENT_ID = os.environ.get(\"CLIENT_ID\")<br>CLIENT_SECRET = os.environ.get(\"CLIENT_SECRET\")<br>SEARCH_SERVICE = os.environ.get(\"SEARCH_SERVICE\")  # without .search.windows.net<br>BLOB_ACCOUNT = os.environ.get(\"BLOB_ACCOUNT\")      # without .blob.core.windows.net<br><br>def test_dns(host):<br>    print(f\"\\n[DNS] Resolving {host}...\")<br>    try:<br>        ip = socket.gethostbyname(host)<br>        print(f\"&#x2705; DNS resolved: {ip}\")<br>        return True<br>    except Exception as e:<br>        print(f\"&#x274c; DNS resolution failed: {e}\")<br>        return False<br><br>def test_https(url):<br>    print(f\"\\n[HTTPS] Connecting to {url}...\")<br>    try:<br>        r = requests.get(url, timeout=10)<br>        print(f\"&#x2705; HTTPS OK: {r.status_code}\")<br>        return True<br>    except Exception as e:<br>        print(f\"&#x274c; HTTPS connection failed: {e}\")<br>        return False<br><br>def test_oauth(scope):<br>    print(f\"\\n[OAuth] Requesting token for scope: {scope}\")<br>    try:<br>        if CLIENT_SECRET:<br>            cred = ClientSecretCredential(TENANT_ID, CLIENT_ID, CLIENT_SECRET)<br>        else:<br>            cred = ManagedIdentityCredential()<br>        token = cred.get_token(scope)<br>        print(f\"&#x2705; Token acquired, length={len(token.token)}\")<br>        return token.token<br>    except Exception as e:<br>        print(f\"&#x274c; OAuth token request failed: {e}\")<br>        return None<br><br>def test_search_api(token):<br>    url = f\"https:\/\/{SEARCH_SERVICE}.search.windows.net\/indexes?api-version=2023-11-01\"<br>    print(f\"\\n[Search API] Calling {url}...\")<br>    try:<br>        r = requests.get(url, headers={\"Authorization\": f\"Bearer {token}\"}, timeout=10)<br>        print(f\"&#x2705; Search API response: {r.status_code}\")<br>        print(r.text[:200] + \"...\")<br>        return True<br>    except Exception as e:<br>        print(f\"&#x274c; Search API call failed: {e}\")<br>        return False<br><br>if __name__ == \"__main__\":<br>    # Hop 1: DNS &amp; HTTPS to Azure Entra ID<br>    test_dns(\"login.microsoftonline.com\")<br>    test_https(f\"https:\/\/login.microsoftonline.com\/{TENANT_ID}\/v2.0\/.well-known\/openid-configuration\")<br><br>    # Hop 2: OAuth Token Request<br>    token = test_oauth(\"https:\/\/search.azure.com\/.default\")<br>    if not token:<br>        print(\"\\n&#x1f6a8; STOP: OAuth failed \u2014 check client ID\/secret, scopes, admin consent, or network access.\")<br>        exit(1)<br><br>    # Hop 3: DNS &amp; HTTPS to Azure AI Search Private Endpoint<br>    test_dns(f\"{SEARCH_SERVICE}.search.windows.net\")<br>    test_https(f\"https:\/\/{SEARCH_SERVICE}.search.windows.net\")<br><br>    # Hop 4: Call Azure AI Search API<br>    test_search_api(token)<br><\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How to Use<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Save this as <code>diagnostic.py<\/code> in your Function App.<\/li>\n\n\n\n<li>Set environment variables in <strong>Azure Function App \u2192 Configuration<\/strong>:\n<ul class=\"wp-block-list\">\n<li><code>TENANT_ID<\/code><\/li>\n\n\n\n<li><code>CLIENT_ID<\/code> \/ <code>CLIENT_SECRET<\/code> (or leave secret empty to test Managed Identity)<\/li>\n\n\n\n<li><code>SEARCH_SERVICE<\/code> (e.g., <code>mysearchservice<\/code>)<\/li>\n\n\n\n<li><code>BLOB_ACCOUNT<\/code> (optional for later Blob tests)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>Run it from <strong>Kudu console<\/strong>:BashCopy code<code>python diagnostic.py<\/code><\/li>\n\n\n\n<li>The script will:\n<ul class=\"wp-block-list\">\n<li>Test <strong>DNS resolution<\/strong> for OAuth and Search endpoints.<\/li>\n\n\n\n<li>Test <strong>HTTPS connectivity<\/strong>.<\/li>\n\n\n\n<li>Attempt <strong>OAuth token acquisition<\/strong>.<\/li>\n\n\n\n<li>Call <strong>Azure AI Search API<\/strong> with the token.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How to Read Results<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Fails at DNS step<\/strong> \u2192 Private DNS zone misconfiguration.<\/li>\n\n\n\n<li><strong>Fails at HTTPS step<\/strong> \u2192 NSG\/firewall blocking outbound.<\/li>\n\n\n\n<li><strong>Fails at OAuth token request<\/strong> \u2192 Wrong credentials, missing admin consent, or blocked public endpoint.<\/li>\n\n\n\n<li><strong>Fails at Search API call<\/strong> \u2192 Token audience mismatch, missing role assignment, or private endpoint DNS issue.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;let\u2019s <strong>proceed<\/strong> and complete this with a <strong>full troubleshooting playbook<\/strong> so you can run the diagnostic script, interpret the results, and fix the root cause quickly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. Run the Diagnostic Script<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use the Python script I gave you in the Function App\u2019s <strong>Kudu console<\/strong> or as a temporary function.<br>It will test:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>DNS resolution<\/strong> to <code>login.microsoftonline.com<\/code> and your Search private endpoint.<\/li>\n\n\n\n<li><strong>HTTPS connectivity<\/strong> to those endpoints.<\/li>\n\n\n\n<li><strong>OAuth token acquisition<\/strong> (client credentials or managed identity).<\/li>\n\n\n\n<li><strong>Azure AI Search API call<\/strong> with the token.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Troubleshooting by Failure Point<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>A. DNS Resolution Fails<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Symptom:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>&#x274c; DNS resolution failed: [Errno ...]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Likely Causes:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Private DNS Zone for <code>microsoftonline.com<\/code> or <code>search.windows.net<\/code> incorrectly pointing to internal IP.<\/li>\n\n\n\n<li>Missing Private DNS Zone link to your VNet for Search or Blob.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Remove any private DNS zone for <code>microsoftonline.com<\/code> (OAuth endpoints must stay public).<\/li>\n\n\n\n<li>Ensure you have a Private DNS Zone for <code>privatelink.search.windows.net<\/code> linked to your VNet for Search private endpoint.<\/li>\n\n\n\n<li>Run:BashCopy code<code>nslookup login.microsoftonline.com nslookup &lt;search-service&gt;.search.windows.net<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>B. HTTPS Connectivity Fails<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Symptom:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>&#x274c; HTTPS connection failed: ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Likely Causes:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>NSG or firewall blocking outbound port 443.<\/li>\n\n\n\n<li>No NAT Gateway or outbound route for VNet-integrated Function App.<\/li>\n\n\n\n<li>TLS interception or SSL handshake failure.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Allow outbound HTTPS to:\n<ul class=\"wp-block-list\">\n<li><code>login.microsoftonline.com<\/code> (OAuth)<\/li>\n\n\n\n<li><code>&lt;search-service&gt;.search.windows.net<\/code> (Search)<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>If using private endpoints, ensure outbound to public Microsoft identity endpoints is still allowed.<\/li>\n\n\n\n<li>Test from Kudu:BashCopy code<code>curl -v https:\/\/login.microsoftonline.com<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C. OAuth Token Request Fails<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Symptom:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>&#x274c; OAuth token request failed: ...\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Likely Causes:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Wrong <code>TENANT_ID<\/code>, <code>CLIENT_ID<\/code>, or <code>CLIENT_SECRET<\/code>.<\/li>\n\n\n\n<li>Missing admin consent for required scopes.<\/li>\n\n\n\n<li>Wrong scope (<code>https:\/\/search.azure.com\/.default<\/code> for Search, <code>https:\/\/storage.azure.com\/.default<\/code> for Blob).<\/li>\n\n\n\n<li>Public endpoint blocked by network rules.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>In <strong>Azure Entra ID \u2192 App Registrations<\/strong>:\n<ul class=\"wp-block-list\">\n<li>Verify credentials.<\/li>\n\n\n\n<li>Add API permissions for Azure AI Search and Blob Storage.<\/li>\n\n\n\n<li>Click <strong>Grant admin consent<\/strong>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>If possible, switch to <strong>Managed Identity<\/strong> to avoid secrets:PythonCopy code<code>from azure.identity import ManagedIdentityCredential cred = ManagedIdentityCredential() token = cred.get_token(\"https:\/\/search.azure.com\/.default\")<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>D. Search API Call Fails<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Symptom:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>&#x274c; Search API call failed: 403 \/ 401\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Likely Causes:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Token audience mismatch (token issued for wrong resource).<\/li>\n\n\n\n<li>Missing <strong>Search Index Data Contributor<\/strong> role for the identity.<\/li>\n\n\n\n<li>Private endpoint DNS misconfiguration.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Assign the Function App\u2019s identity the <strong>Search Index Data Contributor<\/strong> role on the Search service.<\/li>\n\n\n\n<li>Ensure DNS for <code>&lt;search-service&gt;.search.windows.net<\/code> resolves to the private IP.<\/li>\n\n\n\n<li>Test:BashCopy code<code>nslookup &lt;search-service&gt;.search.windows.net<\/code><\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>E. Blob Access Fails (Indexer Stage)<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Symptom:<\/strong> Indexer logs show \u201ccannot access blob\u201d. <strong>Likely Causes:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Missing <strong>Storage Blob Data Reader<\/strong> role for the identity.<\/li>\n\n\n\n<li>Private endpoint DNS misconfiguration for Blob.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fix:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Assign <strong>Storage Blob Data Reader<\/strong> role to the identity.<\/li>\n\n\n\n<li>Ensure DNS for <code>&lt;account&gt;.blob.core.windows.net<\/code> resolves to the private IP.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Recommended Fix Order<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Fix DNS<\/strong> \u2014 make sure public endpoints stay public, private endpoints resolve correctly.<\/li>\n\n\n\n<li><strong>Fix outbound HTTPS<\/strong> \u2014 allow Function App to reach OAuth endpoints.<\/li>\n\n\n\n<li><strong>Fix OAuth config<\/strong> \u2014 correct credentials, scopes, and admin consent.<\/li>\n\n\n\n<li><strong>Fix role assignments<\/strong> \u2014 Search Index Data Contributor + Blob Data Reader.<\/li>\n\n\n\n<li><strong>Retest with script<\/strong> \u2014 confirm all hops pass.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">&#x2705; <strong>Key Insight:<\/strong><br>In private endpoint architectures, <strong>OAuth is the #1 failure point<\/strong> because people accidentally try to route <code>login.microsoftonline.com<\/code> through private DNS, which breaks the public identity flow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">you want to <strong>call an Azure Function<\/strong> but <strong>require the Function Key<\/strong> for authentication, and have that call go <strong>through an API Gateway<\/strong> (like Azure API Management or MCP acting as a gateway).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is a <strong>common pattern<\/strong> when you want to:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Keep the Function App <strong>private<\/strong> (VNet + private endpoint).<\/li>\n\n\n\n<li>Require a <strong>function key<\/strong> for access.<\/li>\n\n\n\n<li>Route all calls through a <strong>gateway<\/strong> for security, logging, and throttling.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>1. How Azure Function Keys Work<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Azure Functions supports <strong>three key types<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Host Keys<\/strong> \u2014 apply to all functions in the app.<\/li>\n\n\n\n<li><strong>Function Keys<\/strong> \u2014 apply to a specific function.<\/li>\n\n\n\n<li><strong>System Keys<\/strong> \u2014 used for system-level triggers.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">When you require a <strong>function key<\/strong>, the caller must include it in the request:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>GET https:\/\/&lt;function-app&gt;.azurewebsites.net\/api\/&lt;function-name&gt;?code=&lt;function-key&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or in the header:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">Copy code<code>x-functions-key: &lt;function-key&gt;\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>2. Architecture with Gateway<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s the flow when using <strong>Azure API Management (APIM)<\/strong> or MCP as a gateway:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">MermaidCopy code<code>sequenceDiagram\n    participant Client as Client App \/ Website\n    participant Gateway as API Gateway (APIM \/ MCP)\n    participant Func as Azure Function App (Private Endpoint)\n\n    Client-&gt;&gt;Gateway: HTTPS Request (No Function Key)\n    Gateway-&gt;&gt;Func: HTTPS Request with Function Key (Private Endpoint)\n    Func--&gt;&gt;Gateway: Response\n    Gateway--&gt;&gt;Client: Response\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>3. Step-by-Step Setup<\/strong><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 1 \u2014 Enable Function Key Requirement<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In Azure Portal \u2192 Function App \u2192 Your Function \u2192 <strong>Function Keys<\/strong>.<\/li>\n\n\n\n<li>Create a new key (e.g., <code>gatewayKey<\/code>).<\/li>\n\n\n\n<li>Copy the key value.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 2 \u2014 Restrict Direct Access<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>If using <strong>private endpoints<\/strong>, the Function App is already inaccessible from the public internet.<\/li>\n\n\n\n<li>If public, use <strong>Access Restrictions<\/strong> to allow only the gateway\u2019s IP.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 3 \u2014 Configure Gateway to Inject Function Key<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>If using Azure API Management:<\/strong><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In APIM \u2192 Create an API that points to your Function App\u2019s private endpoint.<\/li>\n\n\n\n<li>In the <strong>Inbound Processing Policy<\/strong>, add:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">XmlCopy code<code>&lt;set-header name=\"x-functions-key\" exists-action=\"override\"&gt;\n    &lt;value&gt;YOUR_FUNCTION_KEY&lt;\/value&gt;\n&lt;\/set-header&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">or append it to the query string:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">XmlCopy code<code>&lt;set-query-parameter name=\"code\" exists-action=\"override\"&gt;\n    &lt;value&gt;YOUR_FUNCTION_KEY&lt;\/value&gt;\n&lt;\/set-query-parameter&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>If using MCP as a gateway:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>MCP\u2019s outbound call configuration should include the <code>x-functions-key<\/code> header with the stored key.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 4 \u2014 Secure the Gateway<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Require <strong>OAuth\/JWT<\/strong> or API key at the <strong>gateway level<\/strong> for clients.<\/li>\n\n\n\n<li>The <strong>Function Key<\/strong> is never exposed to the client \u2014 only the gateway knows it.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Step 5 \u2014 Test the Flow<\/strong><\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Call the <strong>gateway endpoint<\/strong> without the function key \u2014 should succeed because the gateway injects it.<\/li>\n\n\n\n<li>Call the <strong>Function App directly<\/strong> without the key \u2014 should fail with <code>401 Unauthorized<\/code>.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>4. Benefits of This Pattern<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Key secrecy<\/strong> \u2014 clients never see the Function Key.<\/li>\n\n\n\n<li><strong>Centralized security<\/strong> \u2014 gateway handles authentication, rate limiting, logging.<\/li>\n\n\n\n<li><strong>Private networking<\/strong> \u2014 Function App can be private, only reachable from gateway.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>5. Example APIM Policy<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">XmlCopy code<code>&lt;policies&gt;\n    &lt;inbound&gt;\n        &lt;base \/&gt;\n        &lt;set-header name=\"x-functions-key\" exists-action=\"override\"&gt;\n            &lt;value&gt;{{function-key-secret}}&lt;\/value&gt;\n        &lt;\/set-header&gt;\n    &lt;\/inbound&gt;\n    &lt;backend&gt;\n        &lt;base \/&gt;\n    &lt;\/backend&gt;\n    &lt;outbound&gt;\n        &lt;base \/&gt;\n    &lt;\/outbound&gt;\n&lt;\/policies&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here, <code>{{function-key-secret}}<\/code> is stored in APIM Named Values (secure).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>proceed<\/strong> and combine the <strong>OAuth client authentication<\/strong> with <strong>Function Key injection through the gateway<\/strong> into a single visual so your security\/network team can see exactly how the layers work together.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>OAuth + Function Key Gateway Pattern<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">MermaidCopy code<code>sequenceDiagram\n    participant Client as Client App \/ Website\n    participant Gateway as API Gateway (APIM \/ MCP)\n    participant OAuth as Azure Entra ID (Public Endpoint)\n    participant Func as Azure Function App (Private Endpoint)\n\n    %% Step 1: Client Authenticates\n    Client-&gt;&gt;OAuth: Request OAuth Token (client_credentials \/ auth_code)\n    OAuth--&gt;&gt;Client: Access Token (JWT)\n\n    %% Step 2: Client Calls Gateway\n    Client-&gt;&gt;Gateway: HTTPS Request + Bearer Token\n    Gateway-&gt;&gt;Gateway: Validate JWT (OAuth Scope, Expiry, Signature)\n\n    %% Step 3: Gateway Calls Function App\n    Note over Gateway,Func: Gateway injects Function Key<br>into header or query string\n    Gateway-&gt;&gt;Func: HTTPS Request + x-functions-key (Private Endpoint)\n    Func--&gt;&gt;Gateway: Response\n\n    %% Step 4: Gateway Returns to Client\n    Gateway--&gt;&gt;Client: Response\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>How This Works<\/strong><\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Client Authentication (OAuth)<\/strong>\n<ul class=\"wp-block-list\">\n<li>The client never knows the Function Key.<\/li>\n\n\n\n<li>The client authenticates with Azure Entra ID and gets a <strong>JWT access token<\/strong>.<\/li>\n\n\n\n<li>The token is validated <strong>at the gateway<\/strong>.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Gateway Authorization<\/strong>\n<ul class=\"wp-block-list\">\n<li>Gateway checks token claims (scope, audience, expiry).<\/li>\n\n\n\n<li>If valid, it <strong>injects the Function Key<\/strong> into the backend request.<\/li>\n\n\n\n<li>This ensures only the gateway can call the Function App.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Function App Protection<\/strong>\n<ul class=\"wp-block-list\">\n<li>Function App is <strong>private<\/strong> (VNet + private endpoint).<\/li>\n\n\n\n<li>Requires <strong>Function Key<\/strong> for every request.<\/li>\n\n\n\n<li>Rejects any request without the key.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>Separation of Concerns<\/strong>\n<ul class=\"wp-block-list\">\n<li><strong>OAuth<\/strong> protects the <strong>client-to-gateway<\/strong> path.<\/li>\n\n\n\n<li><strong>Function Key<\/strong> protects the <strong>gateway-to-function<\/strong> path.<\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Security Benefits<\/strong><\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Zero key exposure<\/strong> \u2014 Function Key never leaves the gateway.<\/li>\n\n\n\n<li><strong>Defense in depth<\/strong> \u2014 OAuth + Function Key + Private Endpoint.<\/li>\n\n\n\n<li><strong>Centralized policy control<\/strong> \u2014 Gateway can enforce rate limits, logging, IP restrictions.<\/li>\n\n\n\n<li><strong>Auditable<\/strong> \u2014 All calls logged at the gateway.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Example Azure API Management Policy<\/strong><\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">XmlCopy code<code>&lt;policies&gt;\n  &lt;inbound&gt;\n    &lt;base \/&gt;\n    &lt;!-- Validate OAuth Token --&gt;\n    &lt;validate-jwt header-name=\"Authorization\" failed-validation-httpcode=\"401\" failed-validation-error-message=\"Unauthorized\"&gt;\n      &lt;openid-config url=\"https:\/\/login.microsoftonline.com\/{tenant-id}\/v2.0\/.well-known\/openid-configuration\" \/&gt;\n      &lt;required-claims&gt;\n        &lt;claim name=\"aud\"&gt;\n          &lt;value&gt;api:\/\/your-api-app-id&lt;\/value&gt;\n        &lt;\/claim&gt;\n      &lt;\/required-claims&gt;\n    &lt;\/validate-jwt&gt;\n\n    &lt;!-- Inject Function Key --&gt;\n    &lt;set-header name=\"x-functions-key\" exists-action=\"override\"&gt;\n      &lt;value&gt;{{function-key-secret}}&lt;\/value&gt;\n    &lt;\/set-header&gt;\n  &lt;\/inbound&gt;\n  &lt;backend&gt;\n    &lt;base \/&gt;\n  &lt;\/backend&gt;\n  &lt;outbound&gt;\n    &lt;base \/&gt;\n  &lt;\/outbound&gt;\n&lt;\/policies&gt;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Here:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>{{function-key-secret}}<\/code> is stored securely in APIM Named Values.<\/li>\n\n\n\n<li>JWT validation ensures only authorized clients reach the Function App.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. Sequence Diagram Here\u2019s a Mermaid sequence diagram showing the flow: Mermaid sequenceDiagram participant User as Website (Browser) participant MCP as MCP Service (Private Endpoint) participant Func as Azure Function App participant OAuth as Azure Entra ID (OAuth) participant Search as Azure AI Search (Private Endpoint) participant Blob as Azure Blob Storage (Private Endpoint) User-&gt;&gt;MCP: [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_regular_price":[],"currency_symbol":[],"pagelayer_contact_templates":[],"_pagelayer_content":"","footnotes":""},"categories":[1],"tags":[],"class_list":["post-90","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"post_slider_layout_featured_media_urls":{"thumbnail":"","post_slider_layout_landscape_large":"","post_slider_layout_portrait_large":"","post_slider_layout_square_large":"","post_slider_layout_landscape":"","post_slider_layout_portrait":"","post_slider_layout_square":"","full":""},"_links":{"self":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/90","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/comments?post=90"}],"version-history":[{"count":5,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/90\/revisions"}],"predecessor-version":[{"id":100,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/posts\/90\/revisions\/100"}],"wp:attachment":[{"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/media?parent=90"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/categories?post=90"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/roboticsmaestro.com\/ai\/wp-json\/wp\/v2\/tags?post=90"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}