LangGraph Tutorial (TypeScript): adding authentication for beginners

By Cyprian AaronsUpdated 2026-04-22
langgraphadding-authentication-for-beginnerstypescript

This tutorial shows you how to add authentication to a LangGraph TypeScript app by checking an API key before the graph runs. You need this when your graph is exposed through an HTTP endpoint, a server action, or any internal tool where only approved users should be able to trigger agent behavior.

What You'll Need

  • Node.js 18+
  • A TypeScript project with langgraph installed
  • @langchain/core installed
  • express and zod for the API wrapper
  • An API key or bearer token you want to validate
  • Basic familiarity with LangGraph state, nodes, and edges

Install the packages:

npm install langgraph @langchain/core express zod
npm install -D typescript tsx @types/express @types/node

Step-by-Step

  1. Create a graph that expects authenticated user context in its state.
    The graph itself should not handle login logic; it should only trust a validated userId passed in from the request layer.
import { Annotation, StateGraph, START, END } from "langgraph";

const GraphState = Annotation.Root({
  userId: Annotation<string>(),
  message: Annotation<string>(),
});

const app = new StateGraph(GraphState)
  .addNode("formatMessage", (state) => {
    return {
      message: `Hello ${state.userId}, you said: ${state.message}`,
    };
  })
  .addEdge(START, "formatMessage")
  .addEdge("formatMessage", END);

export const graph = app.compile();
  1. Add a small auth helper that validates a bearer token.
    For beginners, keep this simple and deterministic: compare the incoming token against an environment variable and return the associated user identity if it matches.
export function authenticateRequest(authHeader: string | undefined) {
  const expectedToken = process.env.API_TOKEN;

  if (!expectedToken) {
    throw new Error("API_TOKEN is not set");
  }

  if (!authHeader?.startsWith("Bearer ")) {
    throw new Error("Missing bearer token");
  }

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

  if (token !== expectedToken) {
    throw new Error("Invalid token");
  }

  return { userId: "demo-user" };
}
  1. Wrap the graph in an Express route and reject unauthorized requests before execution.
    This is the important part: authentication happens outside the graph, then the validated identity is injected into the graph input.
import express from "express";
import { z } from "zod";
import { graph } from "./graph";
import { authenticateRequest } from "./auth";

const app = express();
app.use(express.json());

const BodySchema = z.object({
  message: z.string().min(1),
});

app.post("/chat", async (req, res) => {
  try {
    const auth = authenticateRequest(req.headers.authorization);
    const body = BodySchema.parse(req.body);

    const result = await graph.invoke({
      userId: auth.userId,
      message: body.message,
    });

    res.json(result);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unknown error";
    res.status(401).json({ error: message });
  }
});

app.listen(3000, () => {
  console.log("Server listening on http://localhost:3000");
});
  1. Add request-scoped authorization checks if different users should see different behavior.
    Authentication says who the caller is. Authorization decides what that caller can do, so put role or tenant checks in your route before calling the graph.
type AuthContext = {
  userId: string;
  role: "admin" | "user";
};

function authorizeAdmin(auth: AuthContext) {
  if (auth.role !== "admin") {
    throw new Error("Forbidden");
  }
}

app.post("/admin-chat", async (req, res) => {
  try {
    const auth = authenticateRequest(req.headers.authorization) as AuthContext;
    authorizeAdmin(auth);

    const body = BodySchema.parse(req.body);
    const result = await graph.invoke({
      userId: auth.userId,
      message: body.message,
    });

    res.json(result);
  } catch (error) {
    const message = error instanceof Error ? error.message : "Unknown error";
    res.status(403).json({ error: message });
  }
});
  1. Keep secrets out of source control and run the server with an environment variable.
    Use .env locally or your platform secret manager in production; never hardcode tokens in the repo.
export API_TOKEN="dev-secret-token"
npx tsx src/server.ts

Testing It

Start the server and send one request without a token. You should get a 401 response with Missing bearer token. Then send another request with Authorization: Bearer dev-secret-token, and the graph should return a formatted message using the authenticated userId.

Example request:

curl -X POST http://localhost:3000/chat \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dev-secret-token" \
  -d '{"message":"hello"}'

If you want to test failure paths, try an invalid token and confirm that the route never reaches graph.invoke. That tells you authentication is blocking unauthenticated traffic at the edge, which is where it belongs.

Next Steps

  • Replace shared API tokens with JWT verification using jose
  • Add tenant-aware authorization so each request is scoped to one customer account
  • Pass authenticated context through LangGraph state and checkpoints for multi-step workflows

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