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 banner toggles an in-memory React auth flag; when// `authenticated === true`, <CopilotKit headers={...}> injects the// `Authorization: Bearer <demo-token>` header on every request. The runtime// route (/api/copilotkit-auth) rejects any request without the header.//// Backend: reuses the shared CrewAI crew via HttpAgent (same topology as the// other chat cells). The auth gate is enforced in the Next.js runtime, not// in the Python server, so the underlying crew needs no changes.//// Default UX: the page loads authenticated so the initial `/info` handshake// succeeds and the chat is immediately usable. Clicking "Sign out" flips to// the unauthenticated state — the next chat submission (and any re-fetch of// `/info`) will 401, which we surface via the page-level error banner. This// inverts the historical unauth-first flow, which crashed the page on load// when `/info` returned 401 before `onError` handlers could attach.//// Error surfacing: <CopilotChat /> surfaces transport errors inconsistently// across states, so this page additionally captures errors via the// <CopilotKit onError> prop and renders a persistent error banner below the// chat. A local ErrorBoundary guards against any uncaught render-time error// from chat internals in the unauthenticated state so the page never white-// screens — instead, the user sees a clear in-page message.import { Component, useCallback, useMemo, useState } from "react";import type { ErrorInfo, ReactNode } from "react";import { CopilotKit, CopilotChat } from "@copilotkit/react-core/v2";import { useDemoAuth } from "./use-demo-auth";import { AuthBanner } from "./auth-banner";interface ChatErrorBoundaryProps { authenticated: boolean; children: ReactNode;}interface ChatErrorBoundaryState { error: Error | null;}/** * Guards <CopilotChat /> against uncaught render-time errors. If the chat * internals throw (most commonly while the app is in the unauthenticated * state and a transient response payload is missing), we render a clear * in-page message instead of white-screening the entire route. The boundary * resets whenever `authenticated` flips, so signing back in restores the * live chat without requiring a full page reload. */class ChatErrorBoundary extends Component< ChatErrorBoundaryProps, ChatErrorBoundaryState> { state: ChatErrorBoundaryState = { error: null }; static getDerivedStateFromError(error: Error): ChatErrorBoundaryState { return { error }; } componentDidUpdate(prevProps: ChatErrorBoundaryProps): void { // Reset on auth transition so signing in re-mounts a fresh <CopilotChat />. if ( prevProps.authenticated !== this.props.authenticated && this.state.error ) { this.setState({ error: null }); } } componentDidCatch(error: Error, info: ErrorInfo): void { // Keep the console trail for devtools but do not rethrow. console.error("[auth-demo] chat error boundary caught:", error, info); } render(): ReactNode { if (this.state.error) { return ( <div data-testid="auth-demo-chat-boundary" className="flex h-full items-center justify-center p-6 text-center text-sm text-neutral-600" > <div> <p className="font-medium text-neutral-800"> Chat unavailable while signed out </p> <p className="mt-1 text-xs text-neutral-500"> Click Sign in above to restore the conversation. </p> </div> </div> ); } return this.props.children; }}export default function AuthDemoPage() { const auth = useDemoAuth(); const [lastError, setLastError] = useState<string | null>(null); // Clear any stale error when auth state flips (authenticate OR sign out). const authenticate = useCallback(() => { setLastError(null); auth.authenticate(); }, [auth]); const signOut = useCallback(() => { setLastError(null); auth.signOut(); }, [auth]); // Compute headers reactively. The provider reads the latest headers prop // on every request via a useEffect that calls `copilotkit.setHeaders(...)` // whenever the merged headers object changes. const headers = useMemo<Record<string, string>>(() => { const h: Record<string, string> = {}; if (auth.authorizationHeader) { h.Authorization = auth.authorizationHeader; } return h; }, [auth.authorizationHeader]); const onError = useCallback( (errorEvent: { error?: { message?: string; status?: number; statusCode?: number }; context?: { response?: { status?: number } }; }) => { const err = errorEvent?.error; const message = err?.message ?? "Request failed"; const status = err?.status ?? err?.statusCode ?? errorEvent?.context?.response?.status; if (status === 401 || /401|unauthor/i.test(message)) { setLastError( "401 Unauthorized — click Sign in above to restore access.", ); } else { setLastError(message); } }, [], ); return ( <CopilotKit runtimeUrl="/api/copilotkit-auth" agent="auth-demo" headers={headers} onError={onError} useSingleEndpoint={false} > <div className="flex h-screen flex-col gap-3 p-6"> <AuthBanner authenticated={auth.authenticated} onAuthenticate={authenticate} onSignOut={signOut} /> <header> <h1 className="text-lg font-semibold">Authentication</h1> <p className="text-sm text-neutral-600"> The runtime rejects requests without a valid Bearer token via an{" "} <code className="rounded bg-neutral-100 px-1 py-0.5 font-mono text-xs"> onRequest </code>{" "} hook. You start signed in — click Sign out above to exercise the 401 path, then Sign in to restore access. </p> </header> <div className="flex-1 overflow-hidden rounded-md border border-neutral-200"> <ChatErrorBoundary authenticated={auth.authenticated}> <CopilotChat agentId="auth-demo" className="h-full" /> </ChatErrorBoundary> </div> {lastError && ( <div role="alert" data-testid="auth-demo-error" className="rounded border border-red-300 bg-red-50 px-3 py-2 text-xs font-medium text-red-900" > {lastError} </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#
Pass your token via the headers prop on <CopilotKit>. CopilotKit forwards every request with that header attached.
import { CopilotKit } from "@copilotkit/react-core/v2";
<CopilotKit
runtimeUrl="/api/copilotkit"
headers={{
Authorization: `Bearer ${userToken}`,
}}
>
<YourApp />
</CopilotKit>Backend#
Wire authentication into the V2 runtime via the onRequest hook. The hook runs before any agent code and operates on the raw Request, so it's the right place to read the Authorization header, run your verifier, and either let the request through or short-circuit with a 401:
import type { NextRequest } from "next/server";
import {
CopilotRuntime,
createCopilotRuntimeHandler,
} from "@copilotkit/runtime/v2";
const runtime = new CopilotRuntime({ agents: { default: myAgent } });
const handler = createCopilotRuntimeHandler({
runtime,
basePath: "/api/copilotkit",
hooks: {
onRequest: ({ request }) => {
const authHeader = request.headers.get("authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new Response(
JSON.stringify({ error: "unauthorized" }),
{ status: 401, headers: { "content-type": "application/json" } },
);
}
const token = authHeader.slice("Bearer ".length);
const user = verifyJwt(token); // your validation
// attach user to request-scoped context here
},
},
});
export const POST = (req: NextRequest) => handler(req);
export const GET = (req: NextRequest) => handler(req);The V1 Next.js adapter (
copilotRuntimeNextJSAppRouterEndpoint) does not forward thehooksoption. UsecreateCopilotRuntimeHandlerfrom@copilotkit/runtime/v2directly when you need theonRequestgate.
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.
