Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ChrisMcKee1/07f978b5160282a7b02d46fa8ca7d0d1 to your computer and use it in GitHub Desktop.

Select an option

Save ChrisMcKee1/07f978b5160282a7b02d46fa8ca7d0d1 to your computer and use it in GitHub Desktop.
Azure Functions HTTP Trigger Authentication with AI Foundry Agents SDK - Complete Guide

Azure Functions HTTP Trigger Authentication with AI Foundry Agents SDK

Complete Guide Based on Microsoft Official Documentation

Overview

This guide explains how to implement authentication for HTTP-triggered Azure Functions when integrating with the Azure AI Foundry Agents SDK. It covers:

  • Function endpoint authentication (App Service Easy Auth with Microsoft Entra ID)
  • Outbound authentication from functions to Azure resources (Managed Identity)
  • OpenAPI specification integration for agent discovery
  • Bearer token validation patterns

Note: This guide focuses on HTTP triggers. For queue-triggered functions, see the Azure Functions AI Foundry samples documentation.


Table of Contents

  1. Authentication Architecture
  2. HTTP Trigger Function-Level Authentication
  3. App Service Easy Auth (Microsoft Entra ID)
  4. Managed Identity for Outbound Calls
  5. OpenAPI Specification Integration
  6. DefaultAzureCredential Chain
  7. Required Azure RBAC Roles
  8. Complete Code Examples
  9. Local Development Setup
  10. Best Practices
  11. Troubleshooting

Authentication Architecture

How Azure Functions (HTTP Trigger) + AI Foundry Agents Work Together

┌─────────────────────────────┐
│  AI Foundry Agent           │
│  (Uses Entra ID)            │
│  • Has OpenAPI spec         │
│  • Authenticates via MI     │
└──────────┬──────────────────┘
           │ HTTP POST with Bearer token
           ▼
┌──────────────────────────────────────────┐
│  Azure Function (HTTP Trigger)           │
│  ┌────────────────────────────────────┐  │
│  │ App Service Easy Auth              │  │
│  │ • Validates Bearer token           │  │
│  │ • Checks Entra ID identity         │  │
│  └────────────┬───────────────────────┘  │
│               ▼                           │
│  ┌────────────────────────────────────┐  │
│  │ Function Code                      │  │
│  │ • authLevel: "anonymous"*          │  │
│  │ • Reads authenticated user claims  │  │
│  │ • Uses DefaultAzureCredential      │  │
│  └────────────┬───────────────────────┘  │
└───────────────┼──────────────────────────┘
                │ Outbound authentication via Managed Identity
                ▼
┌─────────────────────────────┐
│  Azure Resources            │
│  (Storage, Cosmos, SQL, etc)│
└─────────────────────────────┘

*Important: authLevel: "anonymous" means no function key required. Authentication is handled by Easy Auth at the platform level, which validates Microsoft Entra ID bearer tokens.

Key Points:

  • Inbound authentication: Easy Auth validates bearer tokens from AI Foundry Agent
  • Function code: Uses authLevel: "anonymous" (Easy Auth handles authentication)
  • Outbound authentication: Function uses Managed Identity via DefaultAzureCredential
  • OpenAPI spec: Describes function endpoint for agent discovery

HTTP Trigger Authentication Scenarios

Overview of Authentication Options

For HTTP-triggered Azure Functions with AI Foundry Agents, you have multiple authentication options. Each has different security characteristics and use cases:

Scenario Security Level When to Use AI Foundry Support
1. Easy Auth + Managed Identity ⭐⭐⭐⭐⭐ Highest Production (Recommended) ✅ Yes (via OpenAPI)
2. Function Keys (API Key) ⭐⭐ Low Development/Testing only ✅ Yes (via OpenAPI connection)
3. Service Principal (Client Credentials) ⭐⭐⭐⭐ High Multi-tenant scenarios ✅ Yes (manual token acquisition)
4. Manual Bearer Token Validation ⭐⭐⭐⭐ High Custom auth requirements ⚠️ Manual implementation

Scenario 1: Easy Auth + Managed Identity (RECOMMENDED)

Security Profile

  • Most Secure: Platform-level authentication before code runs
  • Zero Secrets: No keys, passwords, or certificates in code
  • Short-Lived Tokens: Automatic token refresh
  • Built-in: No custom authentication code required

How It Works

AI Foundry Agent (Managed Identity)
    │
    │ 1. Get token for audience: api://<function-app-client-id>
    │
    ▼
POST /api/MyFunction
Authorization: Bearer <MI-token>
    │
    ▼
Easy Auth Layer (Platform)
    │
    │ 2. Validate token with Entra ID
    │ 3. Check audience and issuer
    │
    ▼
Function Code (authLevel: anonymous)
    │
    │ 4. Access authenticated claims
    │ 5. Execute business logic

Configuration

Step 1: Function Configuration

function.json (Python/JavaScript):

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"]
    }
  ]
}

C# Isolated Worker:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using System.Security.Claims;

public class SecureAgentFunction
{
    private readonly ILogger<SecureAgentFunction> _logger;

    public SecureAgentFunction(ILogger<SecureAgentFunction> logger)
    {
        _logger = logger;
    }

    [Function("ProcessAgentRequest")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
    {
        // Easy Auth has ALREADY validated the token
        // Function code only runs if token is valid
        
        var principal = req.HttpContext.User;
        var appId = principal?.FindFirst("appid")?.Value; // AI Foundry App ID
        var userId = principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        
        _logger.LogInformation($"Authenticated request from App: {appId}, User: {userId}");
        
        // Your business logic here
        var result = ProcessRequest(req);
        
        return new OkObjectResult(result);
    }
    
    private object ProcessRequest(HttpRequest req)
    {
        // Process the agent's request
        return new { status = "success", message = "Data processed" };
    }
}

Python:

import azure.functions as func
import logging
import json

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.route(route="process_agent_request", methods=["POST"])
def process_agent_request(req: func.HttpRequest) -> func.HttpResponse:
    # Easy Auth has already validated the token
    # Access authenticated user info from headers
    app_id = req.headers.get('X-MS-CLIENT-PRINCIPAL-ID')
    user_name = req.headers.get('X-MS-CLIENT-PRINCIPAL-NAME')
    
    logging.info(f'Authenticated request from: {user_name} (App: {app_id})')
    
    if not app_id:
        return func.HttpResponse("Unauthorized", status_code=401)
    
    # Your business logic
    try:
        req_body = req.get_json()
        result = process_data(req_body)
        
        return func.HttpResponse(
            json.dumps(result),
            mimetype="application/json",
            status_code=200
        )
    except Exception as e:
        logging.error(f"Error: {e}")
        return func.HttpResponse("Internal error", status_code=500)

def process_data(data):
    return {"status": "success", "data": data}

Step 2: Enable Easy Auth

Azure CLI:

# 1. Create App Registration for Function App
APP_ID=$(az ad app create \
  --display-name "MyFunctionApp-Auth" \
  --sign-in-audience AzureADMyOrg \
  --query appId -o tsv)

# 2. Set Application ID URI
az ad app update \
  --id $APP_ID \
  --identifier-uris "api://$APP_ID"

# 3. Enable Easy Auth on Function App
az webapp auth microsoft create \
  --resource-group <resource-group> \
  --name <function-app-name> \
  --client-id $APP_ID \
  --tenant-id <tenant-id> \
  --allowed-audiences "api://$APP_ID" \
  --token-store true

# 4. Require authentication
az webapp auth update \
  --resource-group <resource-group> \
  --name <function-app-name> \
  --unauthenticated-client-action Return401

Step 3: Register OpenAPI Tool with AI Foundry

OpenAPI Spec:

openapi: 3.0.0
info:
  title: Agent Function API
  version: 1.0.0
servers:
  - url: https://<function-app>.azurewebsites.net
paths:
  /api/ProcessAgentRequest:
    post:
      operationId: processAgentRequest
      summary: Process data from AI agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
      responses:
        '200':
          description: Success
      security:
        - azureAd: []
components:
  securitySchemes:
    azureAd:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
          scopes:
            api://<app-id>/.default: Access function app

Register via Portal:

  1. Go to AI Foundry portal → Your Agent → Actions → Add
  2. Select OpenAPI 3.0 specified tool
  3. Authentication: Managed identity
  4. Audience: api://<app-id>

Scenario 2: Function Keys (API Key Authentication)

Security Profile

  • ⚠️ Lower Security: Static keys can be leaked
  • ⚠️ Manual Rotation: Keys don't expire automatically
  • Simple: Easy to implement
  • ⚠️ Use Only For: Development, testing, or internal tools

How It Works

AI Foundry Agent
    │
    │ 1. Include function key in request
    │
    ▼
POST /api/MyFunction?code=<function-key>
    │
    ▼
Function Runtime
    │
    │ 2. Validate function key
    │
    ▼
Function Code (authLevel: function)
    │
    │ 3. Execute (no user identity)

Configuration

Step 1: Function Configuration

C# Isolated Worker:

[Function("ProcessAgentRequest")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req)
{
    // NO authentication info - just validates function key
    _logger.LogInformation("Function key validated, processing request");
    
    // Your business logic
    var result = ProcessRequest(req);
    
    return new OkObjectResult(result);
}

Python:

app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)

@app.route(route="process_agent_request", methods=["POST"])
def process_agent_request(req: func.HttpRequest) -> func.HttpResponse:
    # Function key already validated by runtime
    logging.info("Processing request with function key auth")
    
    req_body = req.get_json()
    result = process_data(req_body)
    
    return func.HttpResponse(json.dumps(result), mimetype="application/json")

Step 2: Get Function Key

# Get function key
FUNCTION_KEY=$(az functionapp function keys list \
  --resource-group <resource-group> \
  --name <function-app-name> \
  --function-name ProcessAgentRequest \
  --query default -o tsv)

echo "Function URL: https://<function-app>.azurewebsites.net/api/ProcessAgentRequest?code=$FUNCTION_KEY"

Step 3: Register with AI Foundry (Custom Connection)

OpenAPI Spec with API Key:

openapi: 3.0.0
info:
  title: Agent Function API
  version: 1.0.0
servers:
  - url: https://<function-app>.azurewebsites.net
paths:
  /api/ProcessAgentRequest:
    post:
      operationId: processAgentRequest
      parameters:
        - name: code
          in: query
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Success
      security:
        - functionKey: []
components:
  securitySchemes:
    functionKey:
      type: apiKey
      name: code
      in: query

Create Custom Connection in AI Foundry:

  1. Portal → Project → Settings → Connections → + New Connection
  2. Select Custom keys
  3. Enter:
    • Key name: code
    • Value: <function-key>
    • Connection name: function-api-key
  4. Register OpenAPI tool with authentication: Connection → Select function-api-key

Scenario 3: Service Principal (Client Credentials Flow)

Security Profile

  • High Security: Token-based authentication
  • Short-Lived Tokens: Tokens expire
  • ⚠️ Requires Secret Management: Client secret must be secured
  • Use For: Multi-tenant, B2B scenarios, service-to-service

How It Works

AI Foundry Agent (Service Principal)
    │
    │ 1. Get token using client_id + client_secret
    │
    ▼
POST /oauth2/v2.0/token
grant_type=client_credentials
    │
    ▼
Entra ID returns access token
    │
    ▼
POST /api/MyFunction
Authorization: Bearer <token>
    │
    ▼
Function Code (Manual Validation)
    │
    │ 2. Validate token signature
    │ 3. Check audience, issuer, expiration
    │
    ▼
Execute business logic

Configuration

Step 1: Create Service Principal

# Create App Registration
SP_APP_ID=$(az ad app create \
  --display-name "AIFoundryAgentSP" \
  --sign-in-audience AzureADMyOrg \
  --query appId -o tsv)

# Create Service Principal
az ad sp create --id $SP_APP_ID

# Create Client Secret
SP_SECRET=$(az ad app credential reset \
  --id $SP_APP_ID \
  --append \
  --query password -o tsv)

# Set Application ID URI
az ad app update \
  --id $SP_APP_ID \
  --identifier-uris "api://$SP_APP_ID"

# Grant permissions (example: to Function App's app registration)
FUNCTION_APP_ID="<function-app-client-id>"
az ad app permission add \
  --id $SP_APP_ID \
  --api $FUNCTION_APP_ID \
  --api-permissions "<permission-id>=Scope"

az ad app permission grant \
  --id $SP_APP_ID \
  --api $FUNCTION_APP_ID

Step 2: Function Code with Manual Token Validation

C# Isolated Worker:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

public class SecureAgentFunctionSP
{
    private readonly ILogger<SecureAgentFunctionSP> _logger;
    private readonly string _tenantId;
    private readonly string _audience;

    public SecureAgentFunctionSP(ILogger<SecureAgentFunctionSP> logger)
    {
        _logger = logger;
        _tenantId = Environment.GetEnvironmentVariable("TENANT_ID")!;
        _audience = Environment.GetEnvironmentVariable("FUNCTION_APP_CLIENT_ID")!;
    }

    [Function("ProcessAgentRequestSP")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
    {
        // Extract Bearer token
        if (!req.Headers.TryGetValue("Authorization", out var authHeader))
        {
            return new UnauthorizedResult();
        }

        var token = authHeader.ToString().Replace("Bearer ", "");
        
        try
        {
            // Validate token
            var principal = await ValidateTokenAsync(token);
            
            var appId = principal.FindFirst("appid")?.Value;
            _logger.LogInformation($"Authenticated service principal: {appId}");
            
            // Your business logic
            var result = ProcessRequest(req);
            return new OkObjectResult(result);
        }
        catch (SecurityTokenException ex)
        {
            _logger.LogError($"Token validation failed: {ex.Message}");
            return new UnauthorizedResult();
        }
    }

    private async Task<ClaimsPrincipal> ValidateTokenAsync(string token)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        
        // Get signing keys from Microsoft
        var configManager = new Microsoft.IdentityModel.Protocols.ConfigurationManager<OpenIdConnectConfiguration>(
            $"https://login.microsoftonline.com/{_tenantId}/v2.0/.well-known/openid-configuration",
            new OpenIdConnectConfigurationRetriever(),
            new HttpDocumentRetriever());
        
        var config = await configManager.GetConfigurationAsync();
        
        var validationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = $"https://login.microsoftonline.com/{_tenantId}/v2.0",
            
            ValidateAudience = true,
            ValidAudience = $"api://{_audience}",
            
            ValidateIssuerSigningKey = true,
            IssuerSigningKeys = config.SigningKeys,
            
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromMinutes(5)
        };
        
        var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
        return principal;
    }
}

NuGet Packages Required:

<PackageReference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="7.0.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />

Application Settings:

{
  "TENANT_ID": "<your-tenant-id>",
  "FUNCTION_APP_CLIENT_ID": "<function-app-client-id>"
}

Step 3: Agent Calls Function

Manual Token Acquisition:

import requests
import json

# Get access token
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
token_data = {
    "grant_type": "client_credentials",
    "client_id": sp_app_id,
    "client_secret": sp_secret,
    "scope": f"api://{function_app_client_id}/.default"
}

token_response = requests.post(token_url, data=token_data)
access_token = token_response.json()["access_token"]

# Call function with token
headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/json"
}

function_response = requests.post(
    "https://<function-app>.azurewebsites.net/api/ProcessAgentRequestSP",
    headers=headers,
    json={"query": "process this data"}
)

print(function_response.json())

Scenario 4: Manual Bearer Token Validation (Custom Auth)

Security Profile

  • Flexible: Full control over validation logic
  • ⚠️ Complex: Must implement all security checks
  • Use For: Custom token issuers, non-Entra ID scenarios

Configuration

C# Example with Custom Validation:

[Function("ProcessAgentRequestCustom")]
public async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
{
    // Extract token
    if (!req.Headers.TryGetValue("Authorization", out var authHeader))
    {
        return new UnauthorizedResult();
    }

    var token = authHeader.ToString().Replace("Bearer ", "");
    
    // Custom validation logic
    if (!await ValidateCustomTokenAsync(token))
    {
        return new UnauthorizedResult();
    }
    
    // Process request
    return new OkObjectResult(new { status = "success" });
}

private async Task<bool> ValidateCustomTokenAsync(string token)
{
    // Your custom validation:
    // - Check signature against your public key
    // - Validate expiration
    // - Check custom claims
    // - Verify audience
    
    return true; // Implement your logic
}

Comparison Matrix

Feature Easy Auth + MI Function Keys Service Principal Manual Validation
Setup Complexity Medium Low High Very High
Security Level Highest Lowest High Variable
Token Expiration Automatic Never (static) Yes (1 hour) Custom
User Identity ✅ Yes ❌ No ✅ Yes (app identity) ✅ Custom
Zero Secrets ✅ Yes ❌ No ❌ No (client secret) ❌ No
AI Foundry Native ✅ Yes (OpenAPI MI) ✅ Yes (connection) ⚠️ Manual ⚠️ Manual
Production Ready ✅ Yes ❌ No ✅ Yes ⚠️ Depends

Recommendations

For Production (Recommended): Use Scenario 1 (Easy Auth + Managed Identity)

Why:

  • ✅ Highest security - no secrets in code
  • ✅ Platform-managed - less code to maintain
  • ✅ Native AI Foundry support via OpenAPI
  • ✅ Automatic token refresh
  • ✅ Full audit trail (who called what)

For Development/Testing: Use Scenario 2 (Function Keys)

Why:

  • ✅ Quick to set up
  • ✅ Simple debugging
  • ⚠️ DO NOT use in production

For Multi-Tenant B2B: Use Scenario 3 (Service Principal)

Why:

  • ✅ Each tenant gets their own identity
  • ✅ Granular permission control
  • ⚠️ Requires secure secret management (Key Vault)

For Custom Requirements: Use Scenario 4 (Manual Validation)

Why:

  • ✅ Full control over authentication
  • ⚠️ High maintenance burden
  • ⚠️ Easy to introduce security bugs


App Service Easy Auth (Microsoft Entra ID)

What is Easy Auth?

Easy Auth (App Service Authentication/Authorization) is a platform-level feature that validates authentication before your code runs. It's built into Azure App Service and Azure Functions, requiring no code changes.

How it works:

  1. Incoming HTTP request arrives at Function App
  2. Easy Auth intercepts request before it reaches your function code
  3. Validates Authorization: Bearer <token> header against Microsoft Entra ID
  4. If valid: request continues to function code (with authenticated user claims)
  5. If invalid: returns HTTP 401 Unauthorized (function code never executes)

Why Use Easy Auth for AI Foundry Integration?

Benefit Description
Zero Code Changes No authentication logic in function code
Platform-Level Security Validates tokens before code execution
Managed Identity Support AI Foundry Agent uses its managed identity to call function
Short-Lived Tokens More secure than static function keys
Automatic Token Refresh Platform handles token lifecycle

Configuring Easy Auth

Step 1: Enable System-Assigned Managed Identity on AI Foundry Project

Your AI Foundry project needs a managed identity to authenticate to your Function App:

# Get AI Foundry project's managed identity
PROJECT_PRINCIPAL_ID=$(az resource show \
  --ids /subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.MachineLearningServices/workspaces/<project-name> \
  --query identity.principalId -o tsv)

echo "AI Foundry Project Principal ID: $PROJECT_PRINCIPAL_ID"

Step 2: Configure Easy Auth on Function App (Azure Portal)

  1. Navigate to your Function App in Azure portal
  2. Select Authentication from left menu
  3. Click Add identity provider
  4. Select Microsoft as identity provider
  5. Configure settings:
    • Supported account types: "Current tenant - Single tenant" (recommended)
    • Unauthenticated requests: HTTP 401 Unauthorized: recommended for APIs
    • Token store: Enabled (optional, for token refresh)
  6. Under Permissions, note the Application ID URI (format: api://<client-id>)
  7. Click Add

Step 3: Configure Easy Auth via Azure CLI (Alternative)

# Enable Easy Auth with Microsoft Entra ID
az webapp auth microsoft create \
  --resource-group <resource-group> \
  --name <function-app-name> \
  --client-id <app-registration-client-id> \
  --tenant-id <tenant-id> \
  --allowed-audiences "api://<app-registration-client-id>" \
  --token-store true

# Require authentication (reject unauthenticated requests)
az webapp auth update \
  --resource-group <resource-group> \
  --name <function-app-name> \
  --unauthenticated-client-action Return401

Step 4: Grant AI Foundry Project Access to Function App

Assign the AI Foundry project's managed identity permission to call your Function App:

# Get Function App resource ID
FUNCTION_APP_ID=$(az functionapp show \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --query id -o tsv)

# Assign role (use appropriate role, e.g., "Website Contributor" or custom role)
az role assignment create \
  --assignee $PROJECT_PRINCIPAL_ID \
  --role "Website Contributor" \
  --scope $FUNCTION_APP_ID

Note: For production, create a custom role with least-privilege permissions (e.g., only Microsoft.Web/sites/functions/invoke/action).

Accessing Authenticated User Claims in Function Code

Once Easy Auth validates the token, your function can access authenticated user information:

C# Isolated Worker (with ASP.NET Core Integration):

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;

[Function("SecureFunction")]
public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
{
    // Get authenticated user from Easy Auth
    ClaimsPrincipal principal = req.HttpContext.User;
    
    if (!principal.Identity?.IsAuthenticated ?? false)
    {
        return new UnauthorizedResult(); // Should never happen with Easy Auth configured
    }
    
    // Extract claims
    var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var userEmail = principal.FindFirst(ClaimTypes.Email)?.Value;
    var appId = principal.FindFirst("appid")?.Value; // AI Foundry app ID
    
    _logger.LogInformation($"Authenticated user: {userId}, App: {appId}");
    
    // Your business logic here
    return new OkObjectResult(new { message = "Authenticated successfully", userId });
}

Python:

import azure.functions as func
import logging

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)

@app.route(route="secure_function", methods=["POST"])
def secure_function(req: func.HttpRequest) -> func.HttpResponse:
    # Easy Auth adds user claims to headers
    user_id = req.headers.get('X-MS-CLIENT-PRINCIPAL-ID')
    user_name = req.headers.get('X-MS-CLIENT-PRINCIPAL-NAME')
    
    logging.info(f'Authenticated user: {user_id}')
    
    if not user_id:
        return func.HttpResponse("Unauthorized", status_code=401)
    
    # Your business logic here
    return func.HttpResponse(
        f"Authenticated as {user_name}",
        status_code=200
    )

Testing Easy Auth Locally

Important: Easy Auth does not run locally in Azure Functions Core Tools. To test authentication logic:

Option 1: Deploy to Azure

  • Most reliable for testing full authentication flow

Option 2: Mock Authentication Headers

  • Add authentication headers manually for local testing:
curl -X POST http://localhost:7071/api/secure_function \
  -H "X-MS-CLIENT-PRINCIPAL-ID: test-user-id" \
  -H "X-MS-CLIENT-PRINCIPAL-NAME: [email protected]" \
  -H "Content-Type: application/json" \
  -d '{"message": "test"}'

Option 3: Conditional Easy Auth Validation

public IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req)
{
    bool isProduction = Environment.GetEnvironmentVariable("AZURE_FUNCTIONS_ENVIRONMENT") == "Production";
    
    if (isProduction && (!req.HttpContext.User.Identity?.IsAuthenticated ?? true))
    {
        return new UnauthorizedResult();
    }
    
    // Function logic
}

OpenAPI Specification Integration

How AI Foundry Discovers Your HTTP Function

AI Foundry Agents use OpenAPI 3.0 specifications to discover and call your HTTP-triggered functions. The agent:

  1. Reads your OpenAPI spec (provided during tool registration)
  2. Understands available operations (operationId, parameters, schemas)
  3. Authenticates using Managed Identity (gets bearer token)
  4. Calls your function endpoint with Authorization: Bearer <token> header

Creating an OpenAPI Spec for Your Function

Minimal Example

openapi: 3.0.0
info:
  title: My Agent Function API
  version: 1.0.0
servers:
  - url: https://<your-function-app>.azurewebsites.net
    description: Production function app
paths:
  /api/ProcessData:
    post:
      operationId: processData
      summary: Processes data from AI agent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                query:
                  type: string
                  description: User query from agent
                context:
                  type: object
                  description: Additional context
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  result:
                    type: string
                  confidence:
                    type: number
      security:
        - azureAd: []
components:
  securitySchemes:
    azureAd:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
          scopes:
            api://<function-app-client-id>/.default: Access function app

Registering OpenAPI Tool with AI Foundry Agent (Portal)

  1. Navigate to Azure AI Foundry portal
  2. Select your Agent
  3. Scroll to Actions → Click Add
  4. Select OpenAPI 3.0 specified tool
  5. Provide:
    • Name: Descriptive tool name (e.g., "DataProcessor")
    • Description: What the tool does (helps agent decide when to use it)
    • OpenAPI Specification: Paste your OpenAPI YAML/JSON
  6. Authentication: Select Managed identity
    • Audience: api://<function-app-client-id> (from Easy Auth configuration)
  7. Click Add

Registering OpenAPI Tool via SDK (Python)

from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential

# Initialize client
project_client = AIProjectClient.from_connection_string(
    credential=DefaultAzureCredential(),
    conn_str="<project-connection-string>"
)

# Load OpenAPI spec
with open("function-openapi.yaml", "r") as f:
    openapi_spec = f.read()

# Create agent with OpenAPI tool
agent = project_client.agents.create_agent(
    model="gpt-4",
    name="DataProcessorAgent",
    instructions="You help users process data using the DataProcessor function.",
    tools=[
        {
            "type": "openapi",
            "name": "DataProcessor",
            "description": "Processes user data and returns results",
            "spec": openapi_spec,
            "auth": {
                "type": "managed_identity",
                "audience": "api://<function-app-client-id>"
            }
        }
    ]
)

How the Agent Calls Your Function

When the agent decides to use your function:

  1. Agent SDK acquires bearer token using AI Foundry's managed identity:

    POST https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials
    &client_id=<ai-foundry-managed-identity-client-id>
    &scope=api://<function-app-client-id>/.default
  2. Agent calls function with bearer token:

    POST https://<your-function-app>.azurewebsites.net/api/ProcessData
    Authorization: Bearer <access-token>
    Content-Type: application/json
    
    {"query": "What is the weather?", "context": {...}}
  3. Easy Auth validates token before function code runs

  4. Function returns response:

    {"result": "The weather is sunny", "confidence": 0.95}
  5. Agent receives response and incorporates into conversation


Managed Identity for Outbound Calls

What is Managed Identity?

A managed identity is an Azure-managed service principal that allows your Function App to authenticate to Azure resources without storing credentials.

Two Types:

  1. System-Assigned Identity

    • Tied to the Function App lifecycle
    • Automatically deleted when Function App is deleted
    • One per Function App
    • Recommended for single-app scenarios
  2. User-Assigned Identity

    • Independent lifecycle
    • Can be shared across multiple resources
    • Recommended for multi-resource scenarios

Enabling Managed Identity

Azure Portal

  1. Navigate to your Function App
  2. Go to SettingsIdentity
  3. Under System assigned, toggle to On
  4. Click Save
  5. Copy the Object (principal) ID for later use

Azure CLI

# System-assigned identity
az functionapp identity assign \
  --name <function-app-name> \
  --resource-group <resource-group>

# User-assigned identity
az identity create \
  --name <identity-name> \
  --resource-group <resource-group>

az functionapp identity assign \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --identities <identity-resource-id>

DefaultAzureCredential Chain

Authentication Flow

DefaultAzureCredential attempts authentication in this order:

1. Environment Variables
   ├─ AZURE_CLIENT_ID
   ├─ AZURE_TENANT_ID
   └─ AZURE_CLIENT_SECRET

2. Managed Identity
   ├─ System-assigned
   └─ User-assigned

3. Visual Studio (local dev)

4. Azure CLI (local dev)

5. Azure PowerShell (local dev)

Code Examples

System-Assigned Managed Identity (Production)

using Azure.Identity;
using Azure.Storage.Queues;

// Automatically uses managed identity when running in Azure
var credential = new DefaultAzureCredential();

var queueClient = new QueueClient(
    new Uri("https://<storage-account>.queue.core.windows.net/<queue-name>"),
    credential
);

User-Assigned Managed Identity (Production)

var credential = new DefaultAzureCredential(
    new DefaultAzureCredentialOptions
    {
        ManagedIdentityClientId = Environment.GetEnvironmentVariable("MANAGED_IDENTITY_CLIENT_ID")
    }
);

var queueClient = new QueueClient(
    new Uri("https://<storage-account>.queue.core.windows.net/<queue-name>"),
    credential
);

Local Development (Azure CLI)

// Same code works locally after running: az login
var credential = new DefaultAzureCredential();

var queueClient = new QueueClient(
    new Uri("https://<storage-account>.queue.core.windows.net/<queue-name>"),
    credential
);

Environment-Specific Credentials (Advanced)

TokenCredential credential;

if (builder.Environment.IsProduction() || builder.Environment.IsStaging())
{
    // Production: Use managed identity explicitly
    credential = new ManagedIdentityCredential(
        ManagedIdentityId.FromUserAssignedClientId("<client-id>")
    );
}
else
{
    // Local development: Use DefaultAzureCredential
    credential = new DefaultAzureCredential();
}

var queueClient = new QueueClient(queueUri, credential);

Required Azure RBAC Roles

Storage Account Roles

For the Function's managed identity to access Storage Queues:

Role Scope Why Needed
Storage Queue Data Contributor Storage Account Read/write queue messages
Storage Account Contributor Storage Account Optional: Manage queue metadata
Storage Blob Data Contributor Storage Account Optional: If using blob bindings

Assigning Roles

Azure Portal

  1. Navigate to Storage Account
  2. Go to Access Control (IAM)
  3. Click Add role assignment
  4. Select Storage Queue Data Contributor
  5. Under Members, select Managed Identity
  6. Choose your Function App
  7. Click Review + assign

Azure CLI

# Get Function App principal ID
PRINCIPAL_ID=$(az functionapp identity show \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --query principalId -o tsv)

# Get Storage Account ID
STORAGE_ID=$(az storage account show \
  --name <storage-account-name> \
  --resource-group <resource-group> \
  --query id -o tsv)

# Assign role
az role assignment create \
  --assignee $PRINCIPAL_ID \
  --role "Storage Queue Data Contributor" \
  --scope $STORAGE_ID

PowerShell

$FunctionApp = Get-AzFunctionApp `
  -Name <function-app-name> `
  -ResourceGroupName <resource-group>

$StorageAccount = Get-AzStorageAccount `
  -Name <storage-account-name> `
  -ResourceGroupName <resource-group>

New-AzRoleAssignment `
  -ObjectId $FunctionApp.Identity.PrincipalId `
  -RoleDefinitionName "Storage Queue Data Contributor" `
  -Scope $StorageAccount.Id

AI Foundry Project Roles

The AI Foundry Project also needs access to the Storage Account:

# Get AI Foundry project's managed identity
PROJECT_PRINCIPAL_ID=$(az resource show \
  --ids <project-resource-id> \
  --query identity.principalId -o tsv)

# Assign all required roles
for ROLE in \
  "Storage Account Contributor" \
  "Storage Blob Data Contributor" \
  "Storage Queue Data Contributor" \
  "Storage Table Data Contributor" \
  "Storage File Data Privileged Contributor"
do
  az role assignment create \
    --assignee $PROJECT_PRINCIPAL_ID \
    --role "$ROLE" \
    --scope $STORAGE_ID
done

CorrelationId Pattern

Why CorrelationId is Critical

The AI Foundry Agents SDK uses asynchronous communication via queues. The CorrelationId field links responses back to the original request.

❌ Without CorrelationId:

  • Agent can't match responses to requests
  • Function calls fail silently
  • Agent times out waiting for response

✅ With CorrelationId:

  • Agent matches output message to original request
  • Multiple concurrent requests work correctly
  • Proper error handling and retry logic

Data Models

Input Message (from Agent to Function)

public class Arguments
{
    public required string OutputQueueUri { get; set; }
    public required string CorrelationId { get; set; }
    
    // Your custom parameters
    public string Query { get; set; }
    public string Location { get; set; }
}

Output Message (from Function to Agent)

public class Response
{
    public required string Value { get; set; }
    public required string CorrelationId { get; set; } // MUST match input
}

Implementation Example

[Function("ProcessAgentRequest")]
public async Task Run(
    [QueueTrigger("agent-input")] string message,
    FunctionContext context)
{
    var logger = context.GetLogger("ProcessAgentRequest");
    
    // Deserialize input
    var input = JsonSerializer.Deserialize<Arguments>(message);
    logger.LogInformation($"Processing request with CorrelationId: {input.CorrelationId}");
    
    // Process request
    var result = await ProcessBusinessLogic(input.Query);
    
    // Create response with SAME CorrelationId
    var response = new Response
    {
        Value = result,
        CorrelationId = input.CorrelationId // ⚠️ CRITICAL: Must match
    };
    
    // Send to output queue
    var credential = new DefaultAzureCredential();
    var queueClient = new QueueClient(new Uri(input.OutputQueueUri), credential);
    
    await queueClient.SendMessageAsync(
        JsonSerializer.Serialize(response)
    );
    
    logger.LogInformation($"Response sent with CorrelationId: {response.CorrelationId}");
}

Complete Code Examples

C# Isolated Worker (Recommended)

Function Code:

using Azure.Identity;
using Azure.Storage.Queues;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
using System.Text.Json;

namespace FunctionProj
{
    public class AgentFunction
    {
        private readonly ILogger<AgentFunction> _logger;

        public AgentFunction(ILogger<AgentFunction> logger)
        {
            _logger = logger;
        }

        [Function("Foo")]
        public async Task Run(
            [QueueTrigger("azure-function-foo-input")] string inputMessage,
            FunctionContext executionContext)
        {
            _logger.LogInformation("C# Queue trigger function processing message");

            try
            {
                // Parse input message
                var input = JsonSerializer.Deserialize<AgentInput>(inputMessage);
                
                if (input == null)
                {
                    _logger.LogError("Failed to deserialize input message");
                    return;
                }

                _logger.LogInformation($"Processing request with CorrelationId: {input.CorrelationId}");

                // ⚠️ CRITICAL: Use DefaultAzureCredential for passwordless auth
                var credential = new DefaultAzureCredential();
                
                var queueClient = new QueueClient(
                    new Uri(input.OutputQueueUri),
                    credential,
                    new QueueClientOptions { MessageEncoding = QueueMessageEncoding.Base64 }
                );

                // Process your business logic
                var result = ProcessLogic(input.Query);

                // Create response with SAME CorrelationId
                var response = new AgentResponse
                {
                    Value = result,
                    CorrelationId = input.CorrelationId // ⚠️ MUST match input
                };

                // Send to output queue
                var jsonResponse = JsonSerializer.Serialize(response);
                await queueClient.SendMessageAsync(jsonResponse);

                _logger.LogInformation($"Response sent successfully for CorrelationId: {input.CorrelationId}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing agent request");
                throw;
            }
        }

        private string ProcessLogic(string query)
        {
            // Your business logic here
            return $"Processed: {query}";
        }
    }

    // Input data model
    public class AgentInput
    {
        public string OutputQueueUri { get; set; } = string.Empty;
        public string CorrelationId { get; set; } = string.Empty;
        public string Query { get; set; } = string.Empty;
    }

    // Output data model
    public class AgentResponse
    {
        public string Value { get; set; } = string.Empty;
        public string CorrelationId { get; set; } = string.Empty;
    }
}

host.json:

{
  "version": "2.0",
  "logging": {
    "applicationInsights": {
      "samplingSettings": {
        "isEnabled": true,
        "maxTelemetryItemsPerSecond": 20
      }
    }
  },
  "extensions": {
    "queues": {
      "maxPollingInterval": "00:00:02",
      "visibilityTimeout": "00:00:30",
      "batchSize": 16,
      "maxDequeueCount": 5,
      "newBatchThreshold": 8
    }
  }
}

local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "STORAGE_SERVICE_ENDPOINT": "https://<your-storage-account>.queue.core.windows.net"
  }
}

Program.cs:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddApplicationInsightsTelemetryWorkerService();
        services.ConfigureFunctionsApplicationInsights();
    })
    .Build();

host.Run();

Project File (.csproj):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Azure.Identity" Version="1.12.0" />
    <PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.22.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.4" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.5.0" />
    <PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="1.2.0" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

Python Example

function_app.py:

import azure.functions as func
import json
import logging
import os
from azure.identity import DefaultAzureCredential
from azure.storage.queue import QueueClient

app = func.FunctionApp()

@app.queue_trigger(
    arg_name="msg", 
    queue_name="azure-function-foo-input",
    connection="STORAGE_CONNECTION"
)
@app.queue_output(
    arg_name="outputQueue",
    queue_name="azure-function-foo-output",
    connection="STORAGE_CONNECTION"
)
def queue_trigger(inputQueue: func.QueueMessage, outputQueue: func.Out[str]):
    try:
        # Parse input message
        message_payload = json.loads(inputQueue.get_body().decode("utf-8"))
        logging.info(f'Processing message: {json.dumps(message_payload)}')
        
        correlation_id = message_payload.get("CorrelationId")
        location = message_payload.get("location")
        
        # Process business logic
        weather_result = f"Weather is 72 degrees and sunny in {location}"
        
        # Create response with SAME CorrelationId
        response_message = {
            "Value": weather_result,
            "CorrelationId": correlation_id  # ⚠️ MUST match input
        }
        
        logging.info(f'Returning response: {json.dumps(response_message)}')
        outputQueue.set(json.dumps(response_message))
        
    except Exception as e:
        logging.error(f"Error processing message: {e}")
        raise

Python with Explicit Credential:

from azure.identity import DefaultAzureCredential
from azure.storage.queue import QueueClient

# Get storage endpoint from environment
storage_endpoint = os.environ["STORAGE_SERVICE_ENDPOINT"]

# Create credential (uses managed identity in Azure, Azure CLI locally)
credential = DefaultAzureCredential()

# Create queue client with passwordless auth
queue_client = QueueClient(
    account_url=storage_endpoint,
    queue_name="azure-function-foo-output",
    credential=credential
)

# Send message
queue_client.send_message(json.dumps(response_message))

Local Development Setup

Prerequisites

  • Azure CLI installed
  • .NET 8.0 SDK (for C#)
  • Azure Functions Core Tools v4
  • Visual Studio Code with Azure Functions extension

Step 1: Authenticate Azure CLI

# Login to Azure
az login

# Set subscription (if you have multiple)
az account set --subscription <subscription-id>

# Verify authentication
az account show

Step 2: Configure Local Settings

local.settings.json:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "STORAGE_SERVICE_ENDPOINT": "https://<your-storage>.queue.core.windows.net",
    "STORAGE_CONNECTION": "<leave-empty-for-managed-identity>"
  }
}

Step 3: Local Storage Options

Option A: Azure Storage Emulator (Azurite)

# Install Azurite
npm install -g azurite

# Start Azurite
azurite --silent --location c:\azurite --debug c:\azurite\debug.log

# Update local.settings.json
"AzureWebJobsStorage": "UseDevelopmentStorage=true"

Option B: Real Azure Storage (with DefaultAzureCredential)

{
  "Values": {
    "AzureWebJobsStorage__accountName": "<your-storage-account>",
    "AzureWebJobsStorage__credential": "DefaultAzureCredential"
  }
}

Step 4: Test Locally

# Navigate to function project
cd <function-project-folder>

# Run function locally
func start

# Test queue trigger by adding message to queue
az storage message put \
  --queue-name "azure-function-foo-input" \
  --content '{"OutputQueueUri":"https://<storage>.queue.core.windows.net/azure-function-foo-output","CorrelationId":"test-123","Query":"test"}' \
  --account-name <storage-account>

Step 5: Deploy to Azure

# Create Function App with managed identity
az functionapp create \
  --name <function-app-name> \
  --resource-group <resource-group> \
  --consumption-plan-location <location> \
  --runtime dotnet-isolated \
  --functions-version 4 \
  --storage-account <storage-account> \
  --assign-identity

# Deploy code
func azure functionapp publish <function-app-name>

# Verify deployment
func azure functionapp list-functions <function-app-name>

Best Practices

✅ DO

  1. Use anonymous authLevel for queue-triggered functions

    • Queue message access is controlled by RBAC, not function keys
    • Simplifies architecture and improves security
  2. Use DefaultAzureCredential for all Azure SDK clients

    • Works locally (Azure CLI) and in production (managed identity)
    • No code changes needed between environments
  3. Enable system-assigned managed identity

    • Simpler setup for single-function apps
    • Automatically managed by Azure
  4. Assign least-privilege RBAC roles

    • Use Storage Queue Data Contributor, not Owner
    • Scope to specific storage account, not subscription
  5. Always preserve CorrelationId

    • Required for AI Foundry Agent SDK
    • Enables proper async message correlation
  6. Use structured logging

    _logger.LogInformation(
        "Processing request for CorrelationId: {CorrelationId}, Query: {Query}",
        input.CorrelationId,
        input.Query
    );
  7. Implement proper error handling

    try
    {
        await queueClient.SendMessageAsync(response);
    }
    catch (Azure.RequestFailedException ex) when (ex.Status == 403)
    {
        _logger.LogError("Access denied. Check managed identity and RBAC roles.");
        throw;
    }
  8. Use environment variables for configuration

    var storageEndpoint = Environment.GetEnvironmentVariable("STORAGE_SERVICE_ENDPOINT");

❌ DON'T

  1. Don't use connection strings with keys

    // ❌ BAD: Hardcoded connection string
    var queueClient = new QueueClient(
        "DefaultEndpointsProtocol=https;AccountName=...",
        "queue-name"
    );
    
    // ✅ GOOD: Managed identity
    var queueClient = new QueueClient(
        new Uri("https://account.queue.core.windows.net/queue-name"),
        new DefaultAzureCredential()
    );
  2. Don't grant excessive permissions

    • Storage Account Owner
    • Contributor on subscription
    • Storage Queue Data Contributor on storage account
  3. Don't use function keys for queue triggers

    • Function keys don't apply to queue triggers
    • Adds confusion without security benefit
  4. Don't hardcode queue URIs

    // ❌ BAD
    var uri = new Uri("https://mystorageaccount.queue.core.windows.net/myqueue");
    
    // ✅ GOOD
    var uri = new Uri(input.OutputQueueUri);
  5. Don't forget CorrelationId

    • Agent will never receive your response
    • Causes timeouts and failed operations
  6. Don't use production credentials locally

    • Use Azure CLI authentication for local dev
    • Never put service principal secrets in local.settings.json
  7. Don't deploy local.settings.json to Azure

    • Use Application Settings in Azure portal
    • local.settings.json should be in .gitignore

Troubleshooting

Problem: "Access Denied" / 403 Error

Symptoms:

Azure.RequestFailedException: This request is not authorized to perform this operation.
Status: 403 (Forbidden)

Solutions:

  1. Check managed identity is enabled:

    az functionapp identity show \
      --name <function-app-name> \
      --resource-group <resource-group>
  2. Verify RBAC role assignment:

    az role assignment list \
      --assignee <function-principal-id> \
      --scope <storage-account-resource-id>
  3. Assign missing role:

    az role assignment create \
      --assignee <function-principal-id> \
      --role "Storage Queue Data Contributor" \
      --scope <storage-account-resource-id>
  4. Wait for role propagation (can take up to 5 minutes):

    # Retry after waiting

Problem: Agent Not Receiving Response

Symptoms:

  • Function executes successfully
  • No errors in logs
  • Agent times out waiting for response

Solutions:

  1. Verify CorrelationId is preserved:

    _logger.LogInformation(
        "Input CorrelationId: {InputId}, Output CorrelationId: {OutputId}",
        input.CorrelationId,
        response.CorrelationId
    );
    // These MUST match
  2. Check output queue URI:

    _logger.LogInformation("Sending to queue: {QueueUri}", input.OutputQueueUri);
    // Verify this matches agent configuration
  3. Verify message was sent:

    # Check queue has messages
    az storage message peek \
      --queue-name "azure-function-foo-output" \
      --account-name <storage-account>

Problem: DefaultAzureCredential Fails Locally

Symptoms:

Azure.Identity.CredentialUnavailableException: DefaultAzureCredential failed to retrieve a token

Solutions:

  1. Ensure Azure CLI is logged in:

    az login
    az account show
  2. Check for environment variable conflicts:

    # Remove these if set accidentally
    unset AZURE_CLIENT_ID
    unset AZURE_TENANT_ID
    unset AZURE_CLIENT_SECRET
  3. Use explicit credential for debugging:

    // Temporarily use AzureCliCredential to debug
    var credential = new AzureCliCredential();

Problem: Queue Trigger Not Firing

Symptoms:

  • Function deployed successfully
  • Messages in queue
  • Function never executes

Solutions:

  1. Check connection string configuration:

    // In Application Settings
    "AzureWebJobsStorage__accountName": "<storage-account>",
    "AzureWebJobsStorage__credential": "managedidentity"
  2. Verify queue exists:

    az storage queue exists \
      --name "azure-function-foo-input" \
      --account-name <storage-account>
  3. Check function app logs:

    az functionapp log tail \
      --name <function-app-name> \
      --resource-group <resource-group>
  4. Restart function app:

    az functionapp restart \
      --name <function-app-name> \
      --resource-group <resource-group>

Problem: User-Assigned Identity Not Working

Symptoms:

ManagedIdentityCredential authentication unavailable. No Managed Identity endpoint found.

Solutions:

  1. Verify identity is assigned to function:

    az functionapp identity show \
      --name <function-app-name> \
      --resource-group <resource-group>
  2. Set client ID explicitly:

    // Application Settings
    "MANAGED_IDENTITY_CLIENT_ID": "<user-assigned-identity-client-id>"
  3. Update code to use client ID:

    var credential = new DefaultAzureCredential(
        new DefaultAzureCredentialOptions
        {
            ManagedIdentityClientId = Environment.GetEnvironmentVariable("MANAGED_IDENTITY_CLIENT_ID")
        }
    );

Quick Reference Checklist

Initial Setup

  • Enable managed identity on Function App
  • Assign Storage Queue Data Contributor role to Function identity
  • Set authLevel: "anonymous" in function.json or attribute
  • Install Azure.Identity and Azure.Storage.Queues packages
  • Configure storage endpoint in application settings

Function Code

  • Use DefaultAzureCredential for QueueClient
  • Parse CorrelationId from input message
  • Include CorrelationId in response message
  • Use structured logging with CorrelationId
  • Implement try-catch error handling

Local Development

  • Run az login before testing
  • Configure local.settings.json with storage endpoint
  • Test with Azurite or real Azure Storage
  • Verify messages send/receive correctly

Deployment

  • Deploy function code to Azure
  • Configure application settings in portal
  • Verify managed identity is active
  • Test end-to-end with AI Foundry Agent
  • Monitor Application Insights for errors

Additional Resources

Official Microsoft Documentation

Sample Code Repositories

Azure Identity Documentation

AI Foundry SDK


Summary

Key Takeaways:

  1. Function-level authentication: Use authLevel: "anonymous" for queue-triggered functions
  2. Outbound authentication: Use DefaultAzureCredential to access Azure Storage Queues
  3. Identity management: Enable system-assigned managed identity on Function App
  4. RBAC permissions: Assign Storage Queue Data Contributor role to Function identity
  5. Message correlation: Always preserve CorrelationId from input to output for Agent SDK compatibility
  6. Local development: Authenticate with Azure CLI (az login) for seamless dev/prod experience
  7. Security: Never use connection strings with keys; managed identity is more secure and easier to maintain

This pattern provides a secure, maintainable, and cloud-native approach to integrating Azure Functions with the AI Foundry Agents SDK.


Document Version: 1.0
Last Updated: October 6, 2025
Author: Based on Microsoft Official Documentation
License: MIT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment