CrewAI Tutorial (TypeScript): adding authentication for intermediate developers
This tutorial shows how to add authentication to a CrewAI TypeScript agent flow so only verified users can trigger tasks, access tools, or hit downstream APIs. You need this when your agent is exposed through an API, a web app, or any internal service where you must tie requests to a real identity before letting the crew act.
What You'll Need
- •Node.js 20+
- •TypeScript 5+
- •A CrewAI TypeScript project already set up
- •
crewaiinstalled in your project - •An auth provider:
- •Auth0, Clerk, Firebase Auth, Cognito, or your own JWT issuer
- •A signed JWT access token for testing
- •An API route or service layer where your crew is invoked
- •A backend secret for verifying tokens:
- •
AUTH_JWKS_URLorAUTH_ISSUERdepending on your provider
- •
Step-by-Step
- •Start by installing the packages you need for token verification and environment loading. CrewAI handles the agent orchestration; your app owns authentication and passes the verified user into the crew context.
npm install crewai jose dotenv
npm install -D typescript tsx @types/node
- •Define a small auth layer that verifies a bearer token and returns a typed user object. This keeps auth logic out of your agents and makes it easy to reuse across routes.
// auth.ts
import { createRemoteJWKSet, jwtVerify } from "jose";
export type AuthUser = {
sub: string;
email?: string;
roles: string[];
};
const jwks = createRemoteJWKSet(new URL(process.env.AUTH_JWKS_URL!));
const issuer = process.env.AUTH_ISSUER!;
const audience = process.env.AUTH_AUDIENCE!;
export async function verifyBearerToken(token: string): Promise<AuthUser> {
const { payload } = await jwtVerify(token, jwks, { issuer, audience });
return {
sub: String(payload.sub),
email: typeof payload.email === "string" ? payload.email : undefined,
roles: Array.isArray(payload.roles) ? payload.roles.map(String) : [],
};
}
- •Create your crew so it accepts authenticated context instead of reading from globals. The pattern here is simple: pass the verified user into task inputs and use it to scope what the agent can do.
// crew.ts
import { Agent, Task, Crew } from "crewai";
import type { AuthUser } from "./auth";
export function buildSupportCrew(user: AuthUser) {
const agent = new Agent({
name: "Support Analyst",
role: "Customer support assistant",
goal: `Help authenticated user ${user.sub} with account questions`,
backstory: "You only operate on data belonging to the authenticated user.",
});
const task = new Task({
description: `Answer the user's support request for ${user.email ?? user.sub}.`,
expectedOutput: "A concise support response scoped to this user.",
agent,
});
return new Crew({
agents: [agent],
tasks: [task],
});
}
- •Add an authenticated API handler that rejects missing or invalid tokens before CrewAI runs. This is where you enforce access control and prevent unauthenticated requests from ever reaching your tools or agents.
// server.ts
import "dotenv/config";
import http from "node:http";
import { verifyBearerToken } from "./auth";
import { buildSupportCrew } from "./crew";
function getBearerToken(authHeader?: string) {
if (!authHeader?.startsWith("Bearer ")) return null;
return authHeader.slice("Bearer ".length);
}
const server = http.createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/support") {
res.writeHead(404);
return res.end("Not found");
}
try {
const token = getBearerToken(req.headers.authorization);
if (!token) throw new Error("Missing bearer token");
const user = await verifyBearerToken(token);
const crew = buildSupportCrew(user);
const result = await crew.kickoff();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ user, result }));
} catch (err) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err instanceof Error ? err.message : "Unauthorized" }));
}
});
server.listen(3000, () => console.log("Listening on http://localhost:3000"));
- •If you have tools that touch customer data, gate them by role and subject as well. Authentication says who the caller is; authorization decides what that caller can do.
// tool.ts
import type { AuthUser } from "./auth";
export function assertCanReadAccount(user: AuthUser, accountId: string) {
if (!user.roles.includes("support") && !user.roles.includes("admin")) {
throw new Error("Insufficient permissions");
}
if (!accountId.startsWith(user.sub) && !user.roles.includes("admin")) {
throw new Error("Cross-account access denied");
}
}
Testing It
Run the server with your auth environment variables set:
- •
AUTH_JWKS_URL - •
AUTH_ISSUER - •
AUTH_AUDIENCE
Then send a request with a valid JWT in the Authorization header. If everything is wired correctly, unauthenticated requests should return 401, and authenticated requests should return a JSON response containing the verified user plus the CrewAI result.
A good test sequence is:
- •no header →
401 Missing bearer token - •invalid token →
401 - •valid token with correct issuer/audience →
200
If you added tool-level checks, try calling an account outside the authenticated subject. That should fail even if the token is valid; that’s the difference between authentication working and authorization actually being enforced.
Next Steps
- •Add refresh-token handling at your API boundary so sessions stay valid without long-lived access tokens.
- •Move authorization rules into a policy layer if you have multiple crews and multiple roles.
- •Add request logging with
sub,email, and trace IDs so every agent action is auditable.
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