LlamaIndex Tutorial (TypeScript): adding authentication for advanced developers

By Cyprian AaronsUpdated 2026-04-21
llamaindexadding-authentication-for-advanced-developerstypescript

This tutorial shows you how to add authentication to a TypeScript LlamaIndex app so only verified users can query your index or hit downstream tools. You need this when your agent sits behind an API, serves multiple tenants, or exposes sensitive retrieval data that should not be public.

What You'll Need

  • Node.js 18+ and npm
  • A TypeScript project with tsconfig.json
  • LlamaIndex TypeScript packages:
    • llamaindex
    • express
    • jsonwebtoken
    • dotenv
    • zod
  • An OpenAI API key for the index/query example
  • A JWT secret for signing and verifying access tokens
  • Basic familiarity with Express middleware and async/await

Step-by-Step

  1. Set up your project dependencies and environment variables first. The important part is separating model credentials from auth credentials, because they solve different problems.
npm init -y
npm install llamaindex express jsonwebtoken dotenv zod
npm install -D typescript ts-node @types/express @types/jsonwebtoken @types/node
OPENAI_API_KEY=your_openai_key
JWT_SECRET=super-long-random-secret
PORT=3000
  1. Create a small authenticated Express server. This middleware verifies a Bearer token before any request reaches your LlamaIndex query handler.
import express, { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

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

type AuthenticatedRequest = Request & { user?: { sub: string; role: string } };

function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing bearer token" });
  }

  const token = header.slice("Bearer ".length);
  try {
    req.user = jwt.verify(token, process.env.JWT_SECRET!) as { sub: string; role: string };
    next();
  } catch {
    return res.status(401).json({ error: "Invalid token" });
  }
}
  1. Build a minimal LlamaIndex query engine and bind it to the authenticated route. This example uses an in-memory document set, which keeps the code runnable without extra infrastructure.
import {
  Document,
  VectorStoreIndex,
  Settings,
  OpenAI,
} from "llamaindex";

Settings.llm = new OpenAI({
  model: "gpt-4o-mini",
});

const docs = [
  new Document({ text: "Policy A covers accidental damage with a $500 deductible." }),
  new Document({ text: "Policy B covers theft but excludes electronics over $2,000." }),
];

const index = await VectorStoreIndex.fromDocuments(docs);
const queryEngine = index.asQueryEngine();

app.post("/query", requireAuth, async (req: AuthenticatedRequest, res: Response) => {
  const schema = z.object({ question: z.string().min(1) });
  const parsed = schema.safeParse(req.body);

  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }

  const result = await queryEngine.query({
    query: `${parsed.data.question}\n\nUser role: ${req.user?.role ?? "unknown"}`,
  });

  res.json({
    userId: req.user?.sub,
    answer: result.toString(),
  });
});
  1. Add a simple token issuer for testing. In production, this would be your identity provider, not a local endpoint.
import { z } from "zod";

app.post("/login", (req: Request, res: Response) => {
  const schema = z.object({
    username: z.string().min(1),
    password: z.string().min(1),
  });

  const parsed = schema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }

  if (parsed.data.username !== "admin" || parsed.data.password !== "admin123") {
    return res.status(401).json({ error: "Invalid credentials" });
  }

  const token = jwt.sign(
    { sub: parsed.data.username, role: "analyst" },
    process.env.JWT_SECRET!,
    { expiresIn: "1h" }
  );

  res.json({ accessToken: token });
});
  1. Start the server and keep the bootstrap clean. If you skip this part and wire everything into top-level code without awaiting initialization, you’ll eventually get race conditions around index startup.
async function main() {
  const port = Number(process.env.PORT ?? "3000");

  app.get("/health", (_req, res) => res.json({ ok: true }));

  app.listen(port, () => {
    console.log(`Server running on http://localhost:${port}`);
  });
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Testing It

First call /query without a token and confirm you get a 401 Missing bearer token. Then call /login with admin/admin123, copy the returned JWT, and retry /query with Authorization: Bearer <token>. If the setup is correct, the response should include both the authenticated user ID and an answer generated from the indexed documents.

A quick curl flow looks like this:

curl -s http://localhost:3000/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin123"}'
curl -s http://localhost:3000/query \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_TOKEN_HERE' \
  -d '{"question":"What does Policy A cover?"}'

If you want stronger validation, test three cases:

  • No token at all
  • An expired or tampered token
  • A valid token with an allowed role versus a disallowed role

Next Steps

  • Move JWT verification to a shared auth middleware package used by all agent services.
  • Replace local login with OIDC or SAML-backed identity providers like Auth0, Azure AD, or Okta.
  • Add per-user retrieval filters so authenticated users only query documents they are allowed to see.

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