LangGraph Tutorial (TypeScript): adding authentication for advanced developers

By Cyprian AaronsUpdated 2026-04-22
langgraphadding-authentication-for-advanced-developerstypescript

This tutorial shows how to add authentication to a LangGraph TypeScript app so every graph run is tied to a verified user identity. You need this when your agent reads user-specific data, calls protected tools, or must enforce tenant isolation before any node executes.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • @langchain/langgraph
  • @langchain/core
  • zod
  • A LangGraph project already set up with basic graph execution
  • An auth source:
    • JWTs from your backend, or
    • session tokens from your app server
  • A working tsconfig.json with moduleResolution: "NodeNext" or compatible ESM settings

Step-by-Step

  1. Start by defining a typed auth context and a small verifier. In production, this should validate a real token from your API gateway or backend before the graph sees anything else.
import { z } from "zod";

export const AuthContextSchema = z.object({
  userId: z.string(),
  email: z.string().email(),
  roles: z.array(z.string()).default([]),
});

export type AuthContext = z.infer<typeof AuthContextSchema>;

export function verifyAuthHeader(authHeader: string | null): AuthContext {
  if (!authHeader?.startsWith("Bearer ")) {
    throw new Error("Missing bearer token");
  }

  const token = authHeader.slice("Bearer ".length);

  if (token !== process.env.DEMO_TOKEN) {
    throw new Error("Invalid token");
  }

  return AuthContextSchema.parse({
    userId: "user_123",
    email: "user@example.com",
    roles: ["user"],
  });
}
  1. Next, define your graph state so auth is part of the runtime context, not stored in the message history. This keeps identity separate from conversation state and makes authorization checks explicit in each node.
import { Annotation } from "@langchain/langgraph";

export const GraphState = Annotation.Root({
  messages: Annotation<string[]>({
    reducer: (left, right) => left.concat(right),
    default: () => [],
  }),
});

export type GraphStateType = typeof GraphState.State;
  1. Build nodes that read the authenticated user from config.configurable. This is the key pattern in LangGraph TypeScript: pass auth through runtime config, then reject unauthorized access before doing any work.
import { RunnableConfig } from "@langchain/core/runnables";
import { GraphStateType } from "./state";
import { AuthContext } from "./auth";

export async function protectedNode(
  state: GraphStateType,
  config?: RunnableConfig
): Promise<Partial<GraphStateType>> {
  const auth = config?.configurable?.auth as AuthContext | undefined;

  if (!auth) {
    throw new Error("Unauthorized");
  }

  return {
    messages: [
      `Hello ${auth.email}. You have ${auth.roles.join(", ") || "no"} roles.`,
      ...state.messages,
    ],
  };
}
  1. Wire the node into a graph and compile it normally. The graph itself stays generic; authentication happens at invocation time, which is what you want when multiple users share the same deployed graph instance.
import { StateGraph, START, END } from "@langchain/langgraph";
import { GraphState } from "./state";
import { protectedNode } from "./node";

const builder = new StateGraph(GraphState)
  .addNode("protectedNode", protectedNode)
  .addEdge(START, "protectedNode")
  .addEdge("protectedNode", END);

export const graph = builder.compile();
  1. Invoke the graph with auth attached to the config object. In a real app, this would happen inside your HTTP handler after verifying the request token and before calling any tool or model node.
import { graph } from "./graph";
import { verifyAuthHeader } from "./auth";

async function main() {
  process.env.DEMO_TOKEN = "dev-token";

  const auth = verifyAuthHeader("Bearer dev-token");

  const result = await graph.invoke(
    { messages: ["initial input"] },
    {
      configurable: {
        auth,
      },
    }
  );

  console.log(result);
}

main().catch(console.error);

Testing It

Run the example once with DEMO_TOKEN=dev-token and confirm the output includes the authenticated email and role list. Then run it again with an invalid bearer token and verify it fails before any node logic completes.

If you want stronger coverage, add tests for three cases: missing header, invalid token, and valid token. The important thing is that authorization is checked outside the graph boundary and again inside sensitive nodes if they touch privileged resources.

For multi-tenant systems, also test that one user's config cannot be reused across another request context. That catches accidental auth leakage through shared objects or long-lived server state.

Next Steps

  • Add role-based routing with conditional edges so admins and standard users follow different paths.
  • Replace the demo verifier with JWT validation using your IdP's public keys.
  • Pass tenant IDs through configurable and enforce them in every tool call that hits a database or internal API.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides