CopilotKit

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).import { useEffect, useMemo, useState } from "react";import {  CopilotKit,  CopilotChat,  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;}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);  // Clear stale errors as soon as the user re-authenticates. Without this  // the amber error surface would persist after sign-in even though the  // failure is no longer relevant.  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}      onError={(event) => {        setAuthError({          message: event.error?.message ?? String(event.error),          code: event.code,        });      }}    >      <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 && !isAuthenticated && (          <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" />        </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.

frontend/src/app/page.tsx
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:

app/api/copilotkit/[[...slug]]/route.ts
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 the hooks option. Use createCopilotRuntimeHandler from @copilotkit/runtime/v2 directly when you need the onRequest gate.

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 delete

This 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.