CrewAI Tutorial (TypeScript): adding authentication for advanced developers

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

This tutorial shows how to add authentication to a CrewAI TypeScript setup so your agents can safely call protected APIs, internal services, or tenant-scoped tools. You need this when your crew is doing real work against systems that require bearer tokens, API keys, or user-bound OAuth credentials.

What You'll Need

  • Node.js 20+
  • A TypeScript project with crewai installed
  • dotenv for environment variables
  • An auth provider or API that issues a token:
    • static API key
    • OAuth2 client credentials
    • JWT from your backend
  • One protected endpoint to test against
  • These environment variables:
    • AUTH_URL
    • CLIENT_ID
    • CLIENT_SECRET
    • API_BASE_URL

Step-by-Step

  1. Install the dependencies and initialize your project if you haven’t already. Keep auth logic outside your agent definitions so you can rotate credentials without touching crew code.
npm init -y
npm install crewai dotenv
npm install -D typescript tsx @types/node
npx tsc --init
  1. Create a small auth client that fetches and caches an access token. This example uses OAuth2 client credentials, which is the cleanest pattern for service-to-service access.
// src/auth.ts
import "dotenv/config";

type TokenResponse = {
  access_token: string;
  token_type: string;
  expires_in: number;
};

let cachedToken: { value: string; expiresAt: number } | null = null;

export async function getAccessToken(): Promise<string> {
  if (cachedToken && Date.now() < cachedToken.expiresAt) return cachedToken.value;

  const body = new URLSearchParams({
    grant_type: "client_credentials",
    client_id: process.env.CLIENT_ID!,
    client_secret: process.env.CLIENT_SECRET!,
    scope: "api.read",
  });

  const res = await fetch(process.env.AUTH_URL!, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });

  if (!res.ok) throw new Error(`Auth failed: ${res.status} ${await res.text()}`);

  const json = (await res.json()) as TokenResponse;
  cachedToken = {
    value: json.access_token,
    expiresAt: Date.now() + (json.expires_in - 30) * 1000,
  };

  return json.access_token;
}
  1. Wrap your protected API call in a tool function that injects the token at request time. This keeps the agent unaware of auth details and lets you reuse the same tool across multiple crews.
// src/tools.ts
import { getAccessToken } from "./auth";

export async function getCustomerProfile(customerId: string): Promise<string> {
  const token = await getAccessToken();

  const res = await fetch(`${process.env.API_BASE_URL}/customers/${customerId}`, {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: "application/json",
    },
  });

  if (!res.ok) throw new Error(`API failed: ${res.status} ${await res.text()}`);

  return JSON.stringify(await res.json(), null, 2);
}
  1. Build the crew and register the authenticated tool. In CrewAI TypeScript, the important part is that your tool is just an async function the agent can call; the auth happens before the HTTP request leaves your process.
// src/index.ts
import "dotenv/config";
import { Agent, Crew, Task } from "crewai";
import { getCustomerProfile } from "./tools";

async function main() {
  const analyst = new Agent({
    name: "Analyst",
    role: "Customer support analyst",
    goal: "Summarize customer profile data accurately",
    backstory: "You work with authenticated internal systems.",
    tools: [getCustomerProfile],
    verbose: true,
  });

  const task = new Task({
    description: "Fetch customer profile for customer ID CUST-1001 and summarize it.",
    expectedOutput: "A concise summary of account status and recent activity.",
    agent: analyst,
  });

  const crew = new Crew({
    agents: [analyst],
    tasks: [task],
    verbose: true,
  });

  const result = await crew.kickoff();
  console.log(result);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
  1. If you need per-user authentication instead of service credentials, pass the user token into the tool at runtime. This is common in banking and insurance workflows where every request must be scoped to the logged-in user.
// src/user-scoped-tool.ts
export function makeGetPolicyTool(userAccessToken: string) {
  return async function getPolicy(policyId: string): Promise<string> {
    const res = await fetch(`${process.env.API_BASE_URL}/policies/${policyId}`, {
      headers: {
        Authorization: `Bearer ${userAccessToken}`,
        Accept: "application/json",
      },
    });

    if (!res.ok) throw new Error(`API failed: ${res.status} ${await res.text()}`);
    return JSON.stringify(await res.json(), null, 2);
  };
}

Testing It

Run the app with valid environment variables and confirm you get a successful response from the protected endpoint. If auth is broken, you should see either a token request failure or a 401/403 from the downstream API, which is exactly what you want during development.

Use one expired credential on purpose to verify token refresh logic works. Then inspect logs to ensure tokens are never printed; only status codes and request IDs should appear in production logs.

If you’re using per-user tokens, test two different users against two different records. The tool should return only data allowed by that user’s permissions, not whatever the agent asks for.

Next Steps

  • Add retry logic with backoff for transient 401 refresh races and 429 throttling.
  • Move auth into a dedicated service layer so multiple tools can share token caching.
  • Add audit logging with request IDs and subject claims for compliance reviews.

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