Complete Guide Based on Microsoft Official Documentation
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.
- Authentication Architecture
- HTTP Trigger Function-Level Authentication
- App Service Easy Auth (Microsoft Entra ID)
- Managed Identity for Outbound Calls
- OpenAPI Specification Integration
- DefaultAzureCredential Chain
- Required Azure RBAC Roles
- Complete Code Examples
- Local Development Setup
- Best Practices
- Troubleshooting
┌─────────────────────────────┐
│ 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
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 |
- ✅ 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
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
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}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 Return401OpenAPI 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 appRegister via Portal:
- Go to AI Foundry portal → Your Agent → Actions → Add
- Select OpenAPI 3.0 specified tool
- Authentication: Managed identity
- Audience:
api://<app-id>
⚠️ 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
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)
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")# 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"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: queryCreate Custom Connection in AI Foundry:
- Portal → Project → Settings → Connections → + New Connection
- Select Custom keys
- Enter:
- Key name:
code - Value:
<function-key> - Connection name:
function-api-key
- Key name:
- Register OpenAPI tool with authentication: Connection → Select
function-api-key
- ✅ 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
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
# 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_IDC# 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>"
}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())- ✅ Flexible: Full control over validation logic
⚠️ Complex: Must implement all security checks- ✅ Use For: Custom token issuers, non-Entra ID scenarios
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
}| 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) | ||
| Production Ready | ✅ Yes | ❌ No | ✅ Yes |
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)
Why:
- ✅ Quick to set up
- ✅ Simple debugging
⚠️ DO NOT use in production
Why:
- ✅ Each tenant gets their own identity
- ✅ Granular permission control
⚠️ Requires secure secret management (Key Vault)
Why:
- ✅ Full control over authentication
⚠️ High maintenance burden⚠️ Easy to introduce security bugs
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:
- Incoming HTTP request arrives at Function App
- Easy Auth intercepts request before it reaches your function code
- Validates
Authorization: Bearer <token>header against Microsoft Entra ID - If valid: request continues to function code (with authenticated user claims)
- If invalid: returns HTTP 401 Unauthorized (function code never executes)
| 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 |
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"- Navigate to your Function App in Azure portal
- Select Authentication from left menu
- Click Add identity provider
- Select Microsoft as identity provider
- 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)
- Under Permissions, note the Application ID URI (format:
api://<client-id>) - Click Add
# 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 Return401Assign 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_IDNote: For production, create a custom role with least-privilege permissions (e.g., only Microsoft.Web/sites/functions/invoke/action).
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
)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
}AI Foundry Agents use OpenAPI 3.0 specifications to discover and call your HTTP-triggered functions. The agent:
- Reads your OpenAPI spec (provided during tool registration)
- Understands available operations (
operationId, parameters, schemas) - Authenticates using Managed Identity (gets bearer token)
- Calls your function endpoint with
Authorization: Bearer <token>header
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- Navigate to Azure AI Foundry portal
- Select your Agent
- Scroll to Actions → Click Add
- Select OpenAPI 3.0 specified tool
- 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
- Authentication: Select Managed identity
- Audience:
api://<function-app-client-id>(from Easy Auth configuration)
- Audience:
- Click Add
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>"
}
}
]
)When the agent decides to use your function:
-
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
-
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": {...}}
-
Easy Auth validates token before function code runs
-
Function returns response:
{"result": "The weather is sunny", "confidence": 0.95} -
Agent receives response and incorporates into conversation
A managed identity is an Azure-managed service principal that allows your Function App to authenticate to Azure resources without storing credentials.
Two Types:
-
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
-
User-Assigned Identity
- Independent lifecycle
- Can be shared across multiple resources
- ✅ Recommended for multi-resource scenarios
- Navigate to your Function App
- Go to Settings → Identity
- Under System assigned, toggle to On
- Click Save
- Copy the Object (principal) ID for later use
# 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 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)
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
);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
);// 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
);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);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 |
- Navigate to Storage Account
- Go to Access Control (IAM)
- Click Add role assignment
- Select Storage Queue Data Contributor
- Under Members, select Managed Identity
- Choose your Function App
- Click Review + assign
# 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$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.IdThe 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
doneThe 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
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; }
}public class Response
{
public required string Value { get; set; }
public required string CorrelationId { get; set; } // MUST match input
}[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}");
}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>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}")
raisePython 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))- Azure CLI installed
- .NET 8.0 SDK (for C#)
- Azure Functions Core Tools v4
- Visual Studio Code with Azure Functions extension
# Login to Azure
az login
# Set subscription (if you have multiple)
az account set --subscription <subscription-id>
# Verify authentication
az account showlocal.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>"
}
}# 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"{
"Values": {
"AzureWebJobsStorage__accountName": "<your-storage-account>",
"AzureWebJobsStorage__credential": "DefaultAzureCredential"
}
}# 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># 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>-
Use
anonymousauthLevel for queue-triggered functions- Queue message access is controlled by RBAC, not function keys
- Simplifies architecture and improves security
-
Use DefaultAzureCredential for all Azure SDK clients
- Works locally (Azure CLI) and in production (managed identity)
- No code changes needed between environments
-
Enable system-assigned managed identity
- Simpler setup for single-function apps
- Automatically managed by Azure
-
Assign least-privilege RBAC roles
- Use
Storage Queue Data Contributor, notOwner - Scope to specific storage account, not subscription
- Use
-
Always preserve CorrelationId
- Required for AI Foundry Agent SDK
- Enables proper async message correlation
-
Use structured logging
_logger.LogInformation( "Processing request for CorrelationId: {CorrelationId}, Query: {Query}", input.CorrelationId, input.Query );
-
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; }
-
Use environment variables for configuration
var storageEndpoint = Environment.GetEnvironmentVariable("STORAGE_SERVICE_ENDPOINT");
-
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() );
-
Don't grant excessive permissions
- ❌
Storage Account Owner - ❌
Contributoron subscription - ✅
Storage Queue Data Contributoron storage account
- ❌
-
Don't use function keys for queue triggers
- Function keys don't apply to queue triggers
- Adds confusion without security benefit
-
Don't hardcode queue URIs
// ❌ BAD var uri = new Uri("https://mystorageaccount.queue.core.windows.net/myqueue"); // ✅ GOOD var uri = new Uri(input.OutputQueueUri);
-
Don't forget CorrelationId
- Agent will never receive your response
- Causes timeouts and failed operations
-
Don't use production credentials locally
- Use Azure CLI authentication for local dev
- Never put service principal secrets in local.settings.json
-
Don't deploy local.settings.json to Azure
- Use Application Settings in Azure portal
- local.settings.json should be in .gitignore
Symptoms:
Azure.RequestFailedException: This request is not authorized to perform this operation.
Status: 403 (Forbidden)
Solutions:
-
Check managed identity is enabled:
az functionapp identity show \ --name <function-app-name> \ --resource-group <resource-group>
-
Verify RBAC role assignment:
az role assignment list \ --assignee <function-principal-id> \ --scope <storage-account-resource-id>
-
Assign missing role:
az role assignment create \ --assignee <function-principal-id> \ --role "Storage Queue Data Contributor" \ --scope <storage-account-resource-id>
-
Wait for role propagation (can take up to 5 minutes):
# Retry after waiting
Symptoms:
- Function executes successfully
- No errors in logs
- Agent times out waiting for response
Solutions:
-
Verify CorrelationId is preserved:
_logger.LogInformation( "Input CorrelationId: {InputId}, Output CorrelationId: {OutputId}", input.CorrelationId, response.CorrelationId ); // These MUST match
-
Check output queue URI:
_logger.LogInformation("Sending to queue: {QueueUri}", input.OutputQueueUri); // Verify this matches agent configuration
-
Verify message was sent:
# Check queue has messages az storage message peek \ --queue-name "azure-function-foo-output" \ --account-name <storage-account>
Symptoms:
Azure.Identity.CredentialUnavailableException: DefaultAzureCredential failed to retrieve a token
Solutions:
-
Ensure Azure CLI is logged in:
az login az account show
-
Check for environment variable conflicts:
# Remove these if set accidentally unset AZURE_CLIENT_ID unset AZURE_TENANT_ID unset AZURE_CLIENT_SECRET
-
Use explicit credential for debugging:
// Temporarily use AzureCliCredential to debug var credential = new AzureCliCredential();
Symptoms:
- Function deployed successfully
- Messages in queue
- Function never executes
Solutions:
-
Check connection string configuration:
// In Application Settings "AzureWebJobsStorage__accountName": "<storage-account>", "AzureWebJobsStorage__credential": "managedidentity"
-
Verify queue exists:
az storage queue exists \ --name "azure-function-foo-input" \ --account-name <storage-account>
-
Check function app logs:
az functionapp log tail \ --name <function-app-name> \ --resource-group <resource-group>
-
Restart function app:
az functionapp restart \ --name <function-app-name> \ --resource-group <resource-group>
Symptoms:
ManagedIdentityCredential authentication unavailable. No Managed Identity endpoint found.
Solutions:
-
Verify identity is assigned to function:
az functionapp identity show \ --name <function-app-name> \ --resource-group <resource-group>
-
Set client ID explicitly:
// Application Settings "MANAGED_IDENTITY_CLIENT_ID": "<user-assigned-identity-client-id>"
-
Update code to use client ID:
var credential = new DefaultAzureCredential( new DefaultAzureCredentialOptions { ManagedIdentityClientId = Environment.GetEnvironmentVariable("MANAGED_IDENTITY_CLIENT_ID") } );
- Enable managed identity on Function App
- Assign
Storage Queue Data Contributorrole 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
- Use
DefaultAzureCredentialfor QueueClient - Parse CorrelationId from input message
- Include CorrelationId in response message
- Use structured logging with CorrelationId
- Implement try-catch error handling
- Run
az loginbefore testing - Configure local.settings.json with storage endpoint
- Test with Azurite or real Azure Storage
- Verify messages send/receive correctly
- 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
- Azure Functions with AI Foundry Agents
- Azure Functions Authorization Levels
- DefaultAzureCredential Overview
- Managed Identity Best Practices
- Azure Storage Queue Client Library
- Azure Identity client library for .NET
- Azure Identity client library for Python
- Authenticating to Azure with DefaultAzureCredential
Key Takeaways:
- Function-level authentication: Use
authLevel: "anonymous"for queue-triggered functions - Outbound authentication: Use
DefaultAzureCredentialto access Azure Storage Queues - Identity management: Enable system-assigned managed identity on Function App
- RBAC permissions: Assign
Storage Queue Data Contributorrole to Function identity - Message correlation: Always preserve
CorrelationIdfrom input to output for Agent SDK compatibility - Local development: Authenticate with Azure CLI (
az login) for seamless dev/prod experience - 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