LangChain Tutorial (TypeScript): adding authentication for beginners
This tutorial shows how to add authentication to a LangChain TypeScript app so only approved users can call your chain or agent. You need this when your app exposes an LLM-powered API, especially if you want to protect usage, control costs, and keep internal tools off-limits.
What You'll Need
- •Node.js 18+
- •TypeScript 5+
- •A LangChain TypeScript project
- •An OpenAI API key
- •
expressfor the HTTP layer - •
jsonwebtokenfor auth tokens - •
zodfor validating request payloads - •
@langchain/openaiandlangchain - •Basic understanding of async/await and Express middleware
Step-by-Step
- •Install the packages and set up your environment.
We’re going to keep auth outside LangChain itself and put it in the API layer, which is the right place for most production apps.
npm install express jsonwebtoken zod langchain @langchain/openai dotenv
npm install -D typescript tsx @types/express @types/jsonwebtoken @types/node
- •Create a small auth helper that signs and verifies JWTs.
For beginners, this is the simplest production-style pattern: issue a token after login, then require that token on every request.
// auth.ts
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET ?? "dev-secret-change-me";
export type AuthUser = {
sub: string;
email: string;
role: "user" | "admin";
};
export function signToken(user: AuthUser) {
return jwt.sign(user, JWT_SECRET, { expiresIn: "1h" });
}
export function verifyToken(token: string): AuthUser {
return jwt.verify(token, JWT_SECRET) as AuthUser;
}
- •Add an Express middleware that checks the
Authorizationheader.
This middleware blocks unauthenticated requests before they ever reach LangChain, which keeps your chain code clean.
// middleware.ts
import { Request, Response, NextFunction } from "express";
import { verifyToken } from "./auth";
declare global {
namespace Express {
interface Request {
user?: { sub: string; email: string; role: "user" | "admin" };
}
}
}
export function requireAuth(req: Request, res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
return res.status(401).json({ error: "Missing Bearer token" });
}
try {
req.user = verifyToken(header.slice(7));
next();
} catch {
return res.status(401).json({ error: "Invalid or expired token" });
}
}
- •Build a protected LangChain route that uses the authenticated user context.
Here we pass the user identity into the prompt so you can personalize responses or enforce role-based behavior later.
// server.ts
import "dotenv/config";
import express from "express";
import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { RunnableSequence } from "@langchain/core/runnables";
import { requireAuth } from "./middleware";
const app = express();
app.use(express.json());
const model = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 0,
});
const prompt = PromptTemplate.fromTemplate(
"You are a support assistant for {email}. Answer this question clearly:\n\n{question}"
);
const chain = RunnableSequence.from([
prompt,
model,
new StringOutputParser(),
]);
const bodySchema = z.object({
question: z.string().min(1),
});
app.post("/ask", requireAuth, async (req, res) => {
const parsed = bodySchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
const answer = await chain.invoke({
email: req.user!.email,
question: parsed.data.question,
});
res.json({ userId: req.user!.sub, answer });
});
app.listen(3000, () => console.log("Server running on http://localhost:3000"));
- •Add a simple login route for testing so you can mint a token locally.
In real systems this would come from your identity provider, but for beginners this makes the flow easy to verify end-to-end.
// add to server.ts before app.listen(...)
import { signToken } from "./auth";
app.post("/login", (req, res) => {
const schema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
const parsed = schema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: parsed.error.flatten() });
}
const token = signToken({
sub: "user_123",
email: parsed.data.email,
role: "user",
});
res.json({ token });
});
Testing It
Start the server with npx tsx server.ts. Then call /login with any valid email and password to get a JWT back.
Use that token in the Authorization header when calling /ask. If auth is working correctly, requests without a token should return 401, and requests with a valid token should return a LangChain-generated answer.
Example test flow:
curl -X POST http://localhost:3000/login \
-H "Content-Type: application/json" \
-d '{"email":"alice@example.com","password":"secret"}'
Then:
curl -X POST http://localhost:3000/ask \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN_HERE" \
-d '{"question":"What does this app do?"}'
If you want one more sanity check, log req.user inside /ask and confirm it matches the signed identity.
Next Steps
- •Move JWT validation behind your real identity provider like Auth0, Azure AD, or Cognito.
- •Add role-based access control so only admins can call sensitive chains.
- •Pass user IDs into LangChain callbacks or metadata for audit logging and traceability.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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