Integrating AI Agents in SharePoint
AI Agents in SharePoint often stall on two fronts: secure Graph access and reliable orchestration. Teams hardcode keys, expose function keys in SPAs, and skip schema validation—leading to outages and security reviews. This solution shows how to integrate AI Agents in SharePoint using Azure Functions (.NET 8), Azure OpenAI, and React with strict TypeScript, Zod schemas, and Managed Identity—no secrets in code, no function keys.
Prerequisites
- Azure subscription with permissions to assign roles
- .NET 8 SDK
- Node.js 20+
- Azure Functions Core Tools v4
- Azure OpenAI resource (standard or global) with a deployed Chat Completions model (e.g., gpt-4o or gpt-35-turbo)
- Microsoft Entra ID app registration for the SPA (React)
The Solution (Step-by-Step)
1) Architecture Overview
- React SPA authenticates with Microsoft Entra ID (MSAL) and calls an Azure Function over HTTPS.
- Azure Function uses Managed Identity for two things:
- Call Azure OpenAI without API keys.
- Call Microsoft Graph for SharePoint operations (e.g., list items, search content).
- Optional: Retrieve non-secret configuration from Azure Key Vault using Managed Identity.
2) Azure Roles and Permissions
- Function App (system-assigned managed identity):
- On Azure OpenAI resource: Cognitive Services OpenAI User role.
- On Azure Key Vault: Key Vault Secrets User role (only if you store configuration there).
- On Microsoft Graph: Application permissions as needed, e.g., Sites.Read.All (read) or Sites.ReadWrite.All (write). Grant admin consent in Enterprise Applications for the Function’s managed identity service principal.
- SPA app registration:
- Expose an API for your Function App or configure Easy Auth with audience (Application ID URI). The SPA will request tokens for this audience.
3) Azure Function (.NET 8, Isolated Worker) with Managed Identity
// File: Program.cs
// Purpose: Configure DI, Managed Identity credentials, HTTP trigger, and CORS.
using System.Net;
using Azure;
using Azure.AI.OpenAI;
using Azure.Core;
using Azure.Identity; // DefaultAzureCredential
using Azure.Security.KeyVault.Secrets; // Optional: Key Vault
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Middleware;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices(services =>
{
// Use DefaultAzureCredential to support Managed Identity in Azure and dev flows locally.
services.AddSingleton
(_ => new DefaultAzureCredential());
// OpenAI client uses Managed Identity; endpoint comes from configuration (App Setting: AZURE_OPENAI_ENDPOINT).
services.AddSingleton(sp =>
{
var cred = sp.GetRequiredService();
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT not configured.");
return new OpenAIClient(new Uri(endpoint), cred);
});
// Optional Key Vault to fetch non-secret config at runtime.
services.AddSingleton(sp =>
{
var cred = sp.GetRequiredService();
var kvUri = Environment.GetEnvironmentVariable("KEY_VAULT_URI");
return kvUri is null ? null! : new SecretClient(new Uri(kvUri), cred);
});
// Graph client factory using Managed Identity. You can use Graph SDK or raw HTTP with token.
services.AddHttpClient("graph");
services.AddSingleton();
services.AddSingleton();
})
.Build();
await host.RunAsync();
// File: Services.cs
// Purpose: Token acquisition for Graph and agent orchestration including OpenAI and SharePoint.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Azure.AI.OpenAI;
using Azure.Identity;
using Azure.Core;
public interface IGraphTokenProvider
{
// Acquire an application token for Microsoft Graph using Managed Identity.
Task GetTokenAsync(CancellationToken ct);
}
public sealed class GraphTokenProvider(TokenCredential credential) : IGraphTokenProvider
{
private readonly TokenCredential _credential = credential;
private static readonly string[] Scopes = ["https://graph.microsoft.com/.default"]; // Application permissions
public async Task GetTokenAsync(CancellationToken ct)
{
var token = await _credential.GetTokenAsync(new TokenRequestContext(Scopes), ct);
return token.Token;
}
}
public interface IAgentService
{
// Orchestrate: summarize SharePoint content then respond via Azure OpenAI.
Task HandleAsync(AgentRequest request, CancellationToken ct);
}
public sealed class AgentService(OpenAIClient openAi, IHttpClientFactory httpFactory, IGraphTokenProvider tokenProvider, ILogger logger) : IAgentService
{
private readonly OpenAIClient _openAi = openAi;
private readonly IHttpClientFactory _httpFactory = httpFactory;
private readonly IGraphTokenProvider _tokenProvider = tokenProvider;
private readonly ILogger _logger = logger;
public async Task HandleAsync(AgentRequest request, CancellationToken ct)
{
// 1) Fetch SharePoint content via Microsoft Graph based on request.SiteId and query.
var graphClient = _httpFactory.CreateClient("graph");
var token = await _tokenProvider.GetTokenAsync(ct);
graphClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
// Example: Search site for files matching the query. Adjust endpoints to your needs.
var searchPayload = new
{
requests = new[]
{
new
{
entityTypes = new[] { "driveItem" },
query = new { queryString = request.Query },
from = 0,
size = 5,
fields = new[] { "name", "path", "title" }
}
}
};
using var content = new StringContent(JsonSerializer.Serialize(searchPayload), Encoding.UTF8, "application/json");
using var resp = await graphClient.PostAsync("https://graph.microsoft.com/v1.0/search/query", content, ct);
if (!resp.IsSuccessStatusCode)
{
_logger.LogWarning("Graph search failed: {Status}", resp.StatusCode);
}
var graphJson = await resp.Content.ReadAsStringAsync(ct);
// 2) Ask Azure OpenAI to synthesize an answer citing found items.
// Use a deterministic, concise system prompt to guide the agent.
var systemPrompt = "You are a SharePoint-aware assistant. Summarize results and provide next steps. Be concise.";
var chat = new ChatCompletionsOptions
{
Messages =
{
new ChatRequestSystemMessage(systemPrompt),
new ChatRequestUserMessage($"User query: {request.Query}\nGraph results JSON: {graphJson}")
},
Temperature = 0.2f,
MaxTokens = 600
};
var deployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_CHAT_DEPLOYMENT")
?? throw new InvalidOperationException("AZURE_OPENAI_CHAT_DEPLOYMENT not configured.");
var result = await _openAi.GetChatCompletionsAsync(deployment, chat, ct);
var message = result.Value.Choices[0].Message.Content?.Trim() ?? "No response.";
return new AgentResponse(message);
}
}
public sealed record AgentRequest(string SiteId, string Query);
public sealed record AgentResponse(string Message);
// File: Function.cs
// Purpose: HTTP endpoint secured by App Service Authentication. No function keys. Validates input and returns agent response.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
public sealed class AgentFunction(IAgentService agent, ILogger logger)
{
private readonly IAgentService _agent = agent;
private readonly ILogger _logger = logger;
[Function("agent-run")]
public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "agent/run")] HttpRequestData req, FunctionContext ctx)
{
// Authentication is enforced by App Service Authentication (Easy Auth) at the platform layer.
// Validate payload strictly.
try
{
var request = await JsonSerializer.DeserializeAsync(req.Body, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
if (request is null || string.IsNullOrWhiteSpace(request.Query) || string.IsNullOrWhiteSpace(request.SiteId))
{
var bad = req.CreateResponse(HttpStatusCode.BadRequest);
await bad.WriteStringAsync("Invalid payload: 'siteId' and 'query' are required.");
return bad;
}
var result = await _agent.HandleAsync(request, ctx.CancellationToken);
var ok = req.CreateResponse(HttpStatusCode.OK);
await ok.WriteStringAsync(JsonSerializer.Serialize(result));
return ok;
}
catch (Exception ex)
{
_logger.LogError(ex, "Agent invocation failed");
var res = req.CreateResponse(HttpStatusCode.InternalServerError);
await res.WriteStringAsync("Agent failed. Check logs.");
return res;
}
}
}
4) React (TypeScript, strict) with MSAL and Zod
// File: src/agentSchema.ts
// Purpose: Runtime validation for request/response payloads.
import { z } from "zod";
export const AgentRequestSchema = z.object({
siteId: z.string().min(1),
query: z.string().min(1)
});
export type AgentRequest = z.infer;
export const AgentResponseSchema = z.object({
message: z.string()
});
export type AgentResponse = z.infer;
// File: src/msal.ts
// Purpose: Initialize MSAL for SPA login; acquire tokens for the Function App audience.
import { PublicClientApplication, type Configuration } from "@azure/msal-browser";
const config: Configuration = {
auth: {
clientId: import.meta.env.VITE_AAD_CLIENT_ID, // SPA app registration
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_AAD_TENANT_ID}`,
redirectUri: import.meta.env.VITE_REDIRECT_URI
},
cache: { cacheLocation: "sessionStorage" }
};
export const msal = new PublicClientApplication(config);
// File: src/api.ts
// Purpose: Call the Function endpoint with a bearer token; no function keys in URL.
import { msal } from "./msal";
import { AgentRequestSchema, AgentResponseSchema, type AgentRequest, type AgentResponse } from "./agentSchema";
const FUNCTION_SCOPE = import.meta.env.VITE_FUNCTION_AUDIENCE; // e.g., api://YOUR-FUNCTION-APP-APPID/.default
const FUNCTION_BASE = import.meta.env.VITE_FUNCTION_BASE; // e.g., https://your-func.azurewebsites.net
async function acquireToken(): Promise {
const accounts = msal.getAllAccounts();
const account = accounts[0] ?? (await msal.loginPopup({ scopes: [FUNCTION_SCOPE] })).account;
const result = await msal.acquireTokenSilent({ account, scopes: [FUNCTION_SCOPE] });
return result.accessToken;
}
export async function runAgent(input: AgentRequest): Promise {
const parsed = AgentRequestSchema.parse(input); // Validate before sending
const token = await acquireToken(); // MSAL gets an access token for the Function audience
const res = await fetch(`${FUNCTION_BASE}/api/agent/run`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(parsed)
});
if (!res.ok) {
throw new Error(`Agent failed: ${res.status}`);
}
const json = await res.json();
return AgentResponseSchema.parse(json); // Validate response shape
}
// File: src/App.tsx
// Purpose: Simple UI to query the agent against SharePoint content.
import { useState } from "react";
import { runAgent } from "./api";
export default function App() {
const [siteId, setSiteId] = useState("");
const [query, setQuery] = useState("");
const [result, setResult] = useState("");
const [busy, setBusy] = useState(false);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
setBusy(true);
try {
const res = await runAgent({ siteId, query });
setResult(res.message);
} catch (err) {
setResult((err as Error).message);
} finally {
setBusy(false);
}
}
return (
SharePoint AI Agent
{result}
);
}
5) Configuration Notes
- App Settings for Function:
- AZURE_OPENAI_ENDPOINT = https://your-openai-resource.openai.azure.com
- AZURE_OPENAI_CHAT_DEPLOYMENT = your-deployment-name
- Optional KEY_VAULT_URI = https://your-kv.vault.azure.net
- CORS:
- For local: local.settings.json Host:CORS = https://localhost:5173
- In Azure: enable CORS on the Function App for your SPA origins.
- Authentication (Function App):
- Enable App Service Authentication (Login with Microsoft) and set Allowed Token Audiences to your Function App Application ID URI.
- Do not use function keys. Rely on Azure AD tokens.
Best Practices & Security
- Recommendation: Use DefaultAzureCredential everywhere. It enables Managed Identity in Azure and dev auth locally.
- Recommendation: Grant least privilege on Graph (e.g., Sites.Read.All). Avoid tenant-wide write unless necessary.
- Recommendation: Store only non-secret configuration in Key Vault when needed; prefer Managed Identity over storing API keys.
- Recommendation: Validate all inputs/outputs with Zod (SPA) and model binding or explicit checks (Function) to reduce runtime errors.
- Recommendation: Configure CORS explicitly per environment. Avoid wildcard origins in production.
- Recommendation: Monitor with Application Insights. Track dependency calls (Graph, OpenAI) and add custom dimensions for correlation IDs.
- Recommendation: Implement retry with exponential backoff for Graph and OpenAI calls. Consider Polly for transient faults.
- Recommendation: If hosting the SPA on Azure Static Web Apps, use its built-in authentication and route protected calls to the Function with Easy Auth.
Summary
- Securely integrate AI Agents in SharePoint by fronting Graph and Azure OpenAI with an Azure Function using Managed Identity—no keys, no secrets in code.
- Enforce strict schemas with Zod in React and input validation in .NET to reduce errors and improve reliability.
- Harden production with proper RBAC, CORS, Application Insights, and transient fault handling.