Showing posts with label react. Show all posts
Showing posts with label react. Show all posts

Saturday, 7 March 2026

Integrating AI Agents in SharePoint with Azure Functions (.NET 8), Azure OpenAI, and React (TypeScript) using Managed Identity

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

setSiteId(e.target.value)} />
setQuery(e.target.value)} />
{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.

Monday, 26 January 2026

Call a React Component with TypeScript and Zod: A Step-by-Step, Production-Ready Pattern

The fastest way to call a component in React is to render it via JSX and pass strictly typed props. This article shows how to call a component in React with TypeScript and Zod so you get compile-time and runtime safety, clear state management, and production-ready patterns.

The Problem

Developers often "call" (render) a component without strict typing or validation, leading to runtime bugs, unclear state, and hard-to-test UI.

Prerequisites

Node.js 20+, pnpm or npm, React 19, TypeScript 5+, Zod 3+, a modern browser. Ensure tsconfig has strict: true.

The Solution (Step-by-Step)

Step 1: Bootstrap a minimal TypeScript + React app

// package.json (excerpt) - ensures React 19 and strict TS
{
  "name": "react-call-component-ts",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "typescript": "^5.6.0",
    "vite": "^5.0.0",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0"
  }
}
// tsconfig.json - strict mode enabled for maximum safety
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "jsx": "react-jsx",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Step 2: Create a strictly typed child component with runtime validation

// src/components/Greeting.tsx
import React, { memo } from "react";
import { z } from "zod";

// 1) Define compile-time props shape via TypeScript
export type GreetingProps = {
  name: string;                 // Required user name
  mode: "friendly" | "formal";  // Discriminated literal union for behavior
};

// 2) Define runtime schema using Zod for additional safety in production
const greetingPropsSchema = z.object({
  name: z.string().min(1, "name is required"),
  mode: z.union([z.literal("friendly"), z.literal("formal")])
});

// 3) React.memo to avoid unnecessary re-renders when props are stable
export const Greeting = memo(function Greeting(props: GreetingProps) {
  // Validate props at runtime to fail fast in dev and log issues in prod
  const result = greetingPropsSchema.safeParse(props);
  if (!result.success) {
    // Render a small fallback and log schema errors for debugging
    console.error("Greeting props invalid:", result.error.format());
    return Invalid greeting config;
  }

  // Safe, parsed props
  const { name, mode } = result.data;

  // Render based on discriminated union value
  if (mode === "friendly") {
    return 

Hi, {name}! Welcome back.

; } return

Hello, {name}. It is good to see you.

; });

Explanation: We "call" a component in React by placing it in JSX like <Greeting name="Sam" mode="friendly" />. The TypeScript type enforces correct usage at compile time; Zod enforces it at runtime.

Step 3: Manage parent state with discriminated unions and render the child

// src/App.tsx
import React, { useEffect, useState } from "react";
import { Greeting } from "./components/Greeting";

// Discriminated union for page state: guarantees exhaustive checks
type PageState =
  | { kind: "loading" }
  | { kind: "ready"; userName: string }
  | { kind: "error"; message: string };

export function App() {
  const [state, setState] = useState({ kind: "loading" });

  // Simulate fetching the current user, then set ready state
  useEffect(() => {
    const timer = setTimeout(() => {
      // In a real app, replace with a fetch call and proper error handling
      setState({ kind: "ready", userName: "Sam" });
    }, 300);
    return () => clearTimeout(timer);
  }, []);

  // Render different UI based on discriminated union state
  if (state.kind === "loading") {
    return 

Loading…

; } if (state.kind === "error") { return

Error: {state.message}

; } // Key line: this is how you "call" (render) the component with props return (

Dashboard

{/* Rendering a list of components safely */} {(["Ada", "Linus", "Grace"] as const).map((n) => ( ))}
); }

Step 4: Mount the app

// src/main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";

const container = document.getElementById("root");
if (!container) throw new Error("Root container missing");

createRoot(container).render(
  // StrictMode helps surface potential issues
  
    
  
);

Best Practices & Security

Pro-Tip: Use React.memo for presentational components to avoid unnecessary re-renders.

Pro-Tip: Use discriminated unions for UI state to guarantee exhaustive handling and safer refactors.

Pro-Tip: Validate at runtime with Zod for boundary inputs (API responses, query params, environment-driven config).

Pro-Tip: Prefer useCallback and stable prop shapes when passing callbacks to memoized children.

Pro-Tip: Keep components pure; avoid hidden side effects inside render logic.

Security note (front-end): Do not embed secrets in the client. If you integrate with Azure or any backend, call a secured API instead of accessing resources directly from the browser.

Security note (Azure backend integration): Use Managed Identity and DefaultAzureCredential in the server/API, not the frontend. Grant the server's managed identity least-privilege RBAC roles only. Example: for Azure Storage reads, assign Storage Blob Data Reader to the API identity at the specific container scope.

Security note (data flow): Validate user input and API responses at the edge (API) with Zod or similar, then keep the front-end strictly typed.

Summary

• You call a component in React by rendering it in JSX with strictly typed, validated props.

• Discriminated unions make UI state predictable, and React.memo boosts performance.

• For real backends, keep secrets server-side, use Managed Identity with least-privilege RBAC, and validate at the edge.

Testing Quickstart

Test a component render with React Testing Library

// src/components/Greeting.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Greeting } from "./Greeting";

test("renders friendly greeting", () => {
  render(<Greeting name="Sam" mode="friendly" />);
  expect(screen.getByText(/Hi, Sam!/)).toBeInTheDocument();
});

test("renders formal greeting", () => {
  render(<Greeting name="Ada" mode="formal" />);
  expect(screen.getByText(/Hello, Ada\./)).toBeInTheDocument();
});

This test verifies the component is "called" with valid props and renders deterministic output. For invalid props, assert that the fallback appears and console error is triggered.