Authentication
Pass user auth context from your frontend to the agent so it can scope tools, data, and decisions to the signed-in user.
"use client";// Auth demo — framework-native request authentication via the V2 runtime's// `onRequest` hook. The runtime route (/api/copilotkit-auth) rejects any// request whose `Authorization: Bearer <demo-token>` header is missing or// wrong.//// UX shape: the demo defaults to UNAUTHENTICATED on first paint so visitors// land on a clear sign-in card. We don't render `<CopilotKit>` until the user// has signed in at least once — that sidesteps the transport 401 that would// otherwise crash `<CopilotChat>` during its initial `/info` handshake.// After the user signs in once, `<CopilotKit>` stays mounted across the// sign-out → sign-in cycle so the post-sign-out state can actually// demonstrate the runtime rejecting unauthenticated requests in the chat// surface (the whole point of the demo).//// Error surfacing: the post-sign-out 401 is captured via the AGENT-SCOPED// `<CopilotChat onError>` channel, NOT the provider-level `<CopilotKit// onError>` alone. Agent-run errors (`agent_run_failed`) are reliably// delivered to the chat-scoped subscription, whereas the provider-level// handler does not fire for them in this flow — so a demo that relies only// on `<CopilotKit onError>` never renders the rejection banner. We register// the same handler on BOTH channels: `<CopilotKit onError>` covers any// provider-level errors (e.g. the initial `/info` handshake) and// `<CopilotChat onError>` covers agent-run rejections, which is what the// sign-out path produces.import { useCallback, useEffect, useMemo, useState } from "react";import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2";import type { CopilotKitCoreErrorCode } from "@copilotkit/react-core/v2";import { AuthBanner } from "./auth-banner";import { SignInCard } from "./sign-in-card";import { useDemoAuth } from "./use-demo-auth";import { DEMO_TOKEN } from "./demo-token";interface AuthDemoErrorState { message: string; code: CopilotKitCoreErrorCode | string;}interface AuthErrorEvent { error?: { message?: string } | null; code: CopilotKitCoreErrorCode;}export default function AuthDemoPage() { const { isAuthenticated, authorizationHeader, hasEverSignedIn, signIn, signOut, } = useDemoAuth(); const headers = useMemo<Record<string, string>>( () => (authorizationHeader ? { Authorization: authorizationHeader } : {}), [authorizationHeader], ); const [authError, setAuthError] = useState<AuthDemoErrorState | null>(null); // Shared error handler wired to BOTH the provider-level and chat-level // `onError` channels (see the file header for why both are needed). const handleAuthError = useCallback((event: AuthErrorEvent) => { setAuthError({ message: (event.error?.message && event.error.message.trim()) || (event.code ? `Request rejected (${event.code})` : "The request was rejected."), code: event.code, }); }, []); // Clear stale errors as soon as the user re-authenticates. This is the // ONLY thing that gates the amber error surface on auth state — the render // condition below keys off `authError` alone. Coupling the render to a // second `!isAuthenticated` slice (the obvious-but-wrong guard) created a // post-sign-out race: the rejection's `onError` fires and calls // `setAuthError`, but if that commit landed in a render where the auth // state hadn't yet settled to false, `authError && !isAuthenticated` // evaluated false and the banner never appeared. Driving the surface off // `authError` and clearing it here on re-auth removes the cross-slice // ordering dependency: a rejection always renders, and signing back in // always wipes it. useEffect(() => { if (isAuthenticated) setAuthError(null); }, [isAuthenticated]); if (!hasEverSignedIn) { return ( <div className="flex h-screen flex-col"> <SignInCard onSignIn={signIn} /> </div> ); } return ( // `useSingleEndpoint={false}` opts into the V2 multi-endpoint protocol // (separate /info, /agents/<id>/run, etc.), which is what this demo's // runtime route is wired up for. <CopilotKit runtimeUrl="/api/copilotkit-auth" agent="auth-demo" headers={headers} useSingleEndpoint={false} // The dev-only inspector overlay is auto-enabled on localhost after // <CopilotKit> mounts. In this demo that happens only after the first // sign-in, so the post-sign-out Sign in button can sit underneath the // overlay in local/D5 runs even though production never shows it. enableInspector={false} onError={handleAuthError} > <div className="flex h-screen flex-col gap-3 p-6"> <AuthBanner authenticated={isAuthenticated} onSignOut={signOut} onSignIn={() => signIn(DEMO_TOKEN)} /> <header> <h1 className="text-lg font-semibold">Authentication</h1> </header> {authError && ( <div data-testid="auth-demo-error" className="rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-sm text-amber-900" > <strong className="font-semibold"> Runtime rejected the request: </strong>{" "} <span data-testid="auth-demo-error-message"> {authError.message} </span>{" "} <code className="ml-1 rounded bg-amber-100 px-1 py-0.5 font-mono text-xs"> {authError.code} </code> </div> )} <div className="flex-1 overflow-hidden rounded-md border border-neutral-200"> <CopilotChat agentId="auth-demo" className="h-full" onError={handleAuthError} /> </div> </div> </CopilotKit> );}You have a chat surface or a hook driving an agent and you want every agent run to know who the request came from. By the end of this guide, your frontend will forward a token, the runtime will pass it through, and your agent code will read the resulting user info on every turn.
When to use this#
- Multi-tenant apps where the agent reads or writes per-user data.
- Tool gating where some tools should only run for authorised users.
- Audit and billing where every run needs an identity to attribute it to.
- Session-aware UX where the agent's behaviour depends on the user's role or permissions.
If you don't need any of those, skip auth entirely. The agent runs anonymously and the frontend never has to care about tokens.
Frontend#
Microsoft Agent Framework's AG-UI host expects authentication on a request header rather than the runtime properties channel. Pass the token via <CopilotKit headers={...}>:
import { CopilotKit } from "@copilotkit/react-core/v2";
<CopilotKit
runtimeUrl="/api/copilotkit"
headers={{
Authorization: `Bearer ${userToken}`,
}}
>
<YourApp />
</CopilotKit>Backend#
Validation lives at the host process level: ASP.NET Core's JwtBearer middleware on the .NET host, FastAPI middleware on the Python host. Either way, the AG-UI endpoint refuses to dispatch the agent until the token is verified — so by the time your tools run, the user identity is already trustworthy.
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.AGUI.AspNetCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using OpenAI;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["JwtAuthority"];
options.Audience = builder.Configuration["JwtAudience"];
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
string githubToken = builder.Configuration["GitHubToken"]!;
var openAI = new OpenAIClient(
new System.ClientModel.ApiKeyCredential(githubToken),
new OpenAIClientOptions { Endpoint = new Uri("https://models.inference.ai.azure.com") }
);
var agent = openAI.GetChatClient("gpt-5.4-mini")
.CreateAIAgent(name: "AGUIAssistant", instructions: "You are a helpful assistant.");
app.MapAGUI("/", agent).RequireAuthorization();
await app.RunAsync();Settings live in appsettings.json:
{
"JwtAuthority": "https://login.microsoftonline.com/{your-tenant-id}/v2.0",
"JwtAudience": "api://{your-client-id}",
"GitHubToken": "your-github-token-here"
}from fastapi import FastAPI, HTTPException, Request, status
from fastapi.middleware.cors import CORSMiddleware
from agent_framework.ag_ui import add_agent_framework_fastapi_endpoint
from agent import create_agent
import os
app = FastAPI(title="CopilotKit + Microsoft Agent Framework")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
REQUIRED_BEARER_TOKEN = os.getenv("AUTH_BEARER_TOKEN")
@app.middleware("http")
async def auth_middleware(request: Request, call_next):
if REQUIRED_BEARER_TOKEN and request.url.path == "/":
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing bearer token")
token = auth_header.split(" ", 1)[1].strip()
if token != REQUIRED_BEARER_TOKEN:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
return await call_next(request)
chat_client = build_chat_client() # Azure OpenAI or OpenAI
my_agent = create_agent(chat_client)
add_agent_framework_fastapi_endpoint(app=app, agent=my_agent, path="/")Settings live in agent/.env:
AUTH_BEARER_TOKEN=super-secret-demo-tokenAvoid shared-secret bearer tokens in production
Examples that validate against a single shared secret are for local demos only. For production, use proper authentication: validate JWTs with Microsoft.AspNetCore.Authentication.JwtBearer (.NET), or OAuth 2.0 / OpenID Connect JWT validation (Python).
Tool gating#
The most common reason to wire auth is so individual tools can decline to run. Read the resolved user inside the tool's handler and bail if the role doesn't match:
def delete_record(record_id: str, *, user: User):
if "admin" not in user.permissions:
raise PermissionError("admin role required")
# do the deleteThis composes with Human in the loop: gate on auth first, surface a confirmation card next, execute last.
Security checklist#
- Always validate the token on the backend. Never trust the frontend's claim.
- Scope every read and write to the resolved user. Auth context only matters if you actually use it to filter data.
- Don't log raw tokens. Log the resolved user id (or
anonymous) instead. - Use HTTPS in production. The Bearer token is sensitive.
- Refresh strategy. Your frontend is responsible for rotating expired tokens before they reach the agent. CopilotKit doesn't refresh on your behalf.